-
Notifications
You must be signed in to change notification settings - Fork 215
prototype identity client segmentation #6606
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
MitchDickinson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work. Approach looks solid 👏
MitchDickinson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just leaving some drive-by commentary on the interface design. Nice work!
| } | ||
| } | ||
|
|
||
| export class ProdIdentityClient implements IdentityClientInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IdentityServiceClient? "Prod" is a bit of a loaded term and could be confusing
| } | ||
| } | ||
|
|
||
| export class LocalIdentityClient implements IdentityClientInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe "FakeIdentityClient", similar comment as above.
| [key: string]: string | ||
| } | ||
| type TokenRequestConfigResponse = Promise<Result<TokenRequestResult, {error: string; store?: string}>> | ||
| interface IdentityClientInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As someone who has very little identity domain knowledge, here's a couple comments when looking at the interface. I know none of this is stuff you directly introduced, but since we're introducing an abstraction here it's a good forcing function to make sure the abstraction is crystal clear
requestDeviceAuthorizationandpollForDeviceAuthorizationseem to be invoked one-after-the-other, which suggests clients probably don't care about both methods existing and this is actually just one method in disguise. Having an interface where clients are required to call two methods in a particular order to do something useful is called "temporal coupling" and is a smell. Should it be just one method?- I'm sure this is just an artifact of the refactoring, but the param/result types here are confusing. We have IdentityToken which is a zod schema type and TokenRequestConfigResponse which wraps a similarly defined (but not zod) TokenRequestResult. Is this intentional or important? If it's important, I wonder if the interface can expose a simple set of types and we can encapsulate the complexity here.
- What is TokenRequestConfig and how does it relate to IdentityToken? My assumption is that device authorization is the means to get the first token, and then that token can be refreshed/exchanged via the tokenRequest function? If that's right, I would expect a stronger link in the type system between what tokenRequest accepts (
[key: string]: stringdoesn't tell me much) and what the device authorization method returns. As a consumer of the interface it's pretty unclear how I would use tokenRequest. The type system doesn't make it obvious whether I should be calling for device authorization or tokenRequest so it feels like there's still a lot of "you must know how this works internally" going on before the interface can be used.
(I'm sure some of these comments lead to deep rabbit holes and I don't think we necessarily need to address them all in scope of this project, but good to have the convo anyway)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will have to look into the suggestions, but at least the temporal coupling point seems very valid. Will consider this, thanks.
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationspackages/cli-kit/dist/public/node/api/identity-client.d.tsimport { ExchangeScopes } from '../../../private/node/session/exchange.js';
import { ApplicationToken } from '../../../private/node/session/schema.js';
import { zod } from '@shopify/cli-kit/node/schema';
/**
* The schema represents an Identity token.
*/
declare const IdentityTokenSchema: zod.ZodObject<{
accessToken: zod.ZodString;
refreshToken: zod.ZodString;
expiresAt: zod.ZodEffects<zod.ZodDate, Date, unknown>;
scopes: zod.ZodArray<zod.ZodString, "many">;
userId: zod.ZodString;
alias: zod.ZodOptional<zod.ZodString>;
}, "strip", zod.ZodTypeAny, {
accessToken: string;
refreshToken: string;
scopes: string[];
expiresAt: Date;
userId: string;
alias?: string | undefined;
}, {
accessToken: string;
refreshToken: string;
scopes: string[];
userId: string;
expiresAt?: unknown;
alias?: string | undefined;
}>;
export type IdentityToken = zod.infer<typeof IdentityTokenSchema>;
export interface DeviceAuthorizationResponse {
deviceCode: string;
userCode: string;
verificationUri: string;
expiresIn: number;
verificationUriComplete?: string;
interval?: number;
}
type ExchangeAccessTokenResponse = Promise<{
[x: string]: ApplicationToken;
}>;
/**
* @returns Something.
*/
export declare function clientId(): string;
declare abstract class IdentityClient {
authTokenPrefix: string;
constructor();
abstract requestDeviceAuthorization(scopes: string[]): Promise<DeviceAuthorizationResponse>;
abstract pollForDeviceAuthorization(deviceAuth: DeviceAuthorizationResponse): Promise<IdentityToken>;
abstract exchangeAccessForApplicationTokens(identityToken: IdentityToken, scopes: ExchangeScopes, store?: string): ExchangeAccessTokenResponse;
abstract refreshAccessToken(currentToken: IdentityToken): Promise<IdentityToken>;
}
export declare class ProdIdentityClient extends IdentityClient {
/**
* Initiate a device authorization flow.
* This will return a DeviceAuthorizationResponse containing the URL where user
* should go to authorize the device without the need of a callback to the CLI.
*
* Also returns a `deviceCode` used for polling the token endpoint in the next step.
*
* @param scopes - The scopes to request.
* @returns An object with the device authorization response.
*/
requestDeviceAuthorization(scopes: string[]): Promise<DeviceAuthorizationResponse>;
/**
* Poll the Oauth token endpoint with the device code obtained from a DeviceAuthorizationResponse.
* The endpoint will return `authorization_pending` until the user completes the auth flow in the browser.
* Once the user completes the auth flow, the endpoint will return the identity token.
*
* Timeout for the polling is defined by the server and is around 600 seconds.
*
* @param deviceAuth - DeviceAuth.
* @returns The identity token.
*/
pollForDeviceAuthorization(deviceAuth: DeviceAuthorizationResponse): Promise<IdentityToken>;
exchangeAccessForApplicationTokens(identityToken: IdentityToken, scopes: ExchangeScopes, store?: string): ExchangeAccessTokenResponse;
/**
* Given an expired access token, refresh it to get a new one.
*
* @param currentToken - CurrentToken.
* @returns - Identity token.
*/
refreshAccessToken(currentToken: IdentityToken): Promise<IdentityToken>;
}
export declare class LocalIdentityClient extends IdentityClient {
private readonly mockUserId;
private readonly mockSessionId;
private readonly mockDeviceUuid;
requestDeviceAuthorization(_scopes: string[]): Promise<DeviceAuthorizationResponse>;
pollForDeviceAuthorization(_deviceAuth: DeviceAuthorizationResponse): Promise<IdentityToken>;
exchangeAccessForApplicationTokens(identityToken: IdentityToken, _scopes: ExchangeScopes, _store?: string): ExchangeAccessTokenResponse;
refreshAccessToken(currentToken: IdentityToken): Promise<IdentityToken>;
}
export declare function getIdentityClient(): IdentityClient;
export {};
Existing type declarationspackages/cli-kit/dist/private/node/session/exchange.d.ts@@ -1,7 +1,7 @@
import { ApplicationToken, IdentityToken } from './schema.js';
import { API } from '../api.js';
import { Result } from '../../../public/node/result.js';
-import { ExtendableError } from '../../../public/node/error.js';
+import { AbortError, ExtendableError } from '../../../public/node/error.js';
export declare class InvalidGrantError extends ExtendableError {
}
export declare class InvalidRequestError extends ExtendableError {
@@ -65,4 +65,22 @@ export declare function exchangeDeviceCodeForAccessToken(deviceCode: string): Pr
export declare function requestAppToken(api: API, token: string, scopes?: string[], store?: string): Promise<{
[x: string]: ApplicationToken;
}>;
+interface TokenRequestResult {
+ access_token: string;
+ expires_in: number;
+ refresh_token: string;
+ scope: string;
+ id_token?: string;
+}
+export declare function tokenRequestErrorHandler({ error, store }: {
+ error: string;
+ store?: string;
+}): AbortError | InvalidGrantError | InvalidRequestError;
+export declare function tokenRequest(params: {
+ [key: string]: string;
+}): Promise<Result<TokenRequestResult, {
+ error: string;
+ store?: string;
+}>>;
+export declare function buildIdentityToken(result: TokenRequestResult, existingUserId?: string, existingAlias?: string): IdentityToken;
export {};
\ No newline at end of file
packages/cli-kit/dist/private/node/session/identity.d.ts@@ -1,3 +1,2 @@
import { API } from '../api.js';
-export declare function clientId(): string;
export declare function applicationId(api: API): string;
\ No newline at end of file
packages/cli-kit/dist/public/node/api/business-platform.d.ts@@ -1,6 +1,13 @@
import { CacheOptions, GraphQLVariables, UnauthorizedHandler } from './graphql.js';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { Variables } from 'graphql-request';
+/**
+ * Fetches the user's email from the Business Platform API.
+ *
+ * @param businessPlatformToken - The business platform token.
+ * @returns The user's email address or undefined if not found.
+ */
+declare function fetchEmail(businessPlatformToken: string | undefined): Promise<string | undefined>;
/**
* Executes a GraphQL query against the Business Platform Destinations API.
*
@@ -30,7 +37,7 @@ export interface BusinessPlatformRequestOptions<TResult, TVariables extends Vari
* @param options - The options for the request.
* @returns The response of the query of generic type <TResult>.
*/
-export declare function businessPlatformRequestDoc<TResult, TVariables extends Variables>(options: BusinessPlatformRequestOptions<TResult, TVariables>): Promise<TResult>;
+declare function businessPlatformRequestDoc<TResult, TVariables extends Variables>(options: BusinessPlatformRequestOptions<TResult, TVariables>): Promise<TResult>;
export interface BusinessPlatformOrganizationsRequestNonTypedOptions {
query: string;
token: string;
@@ -44,7 +51,7 @@ export interface BusinessPlatformOrganizationsRequestNonTypedOptions {
* @param options - The options for the request.
* @returns The response of the query of generic type <T>.
*/
-export declare function businessPlatformOrganizationsRequest<T>(options: BusinessPlatformOrganizationsRequestNonTypedOptions): Promise<T>;
+declare function businessPlatformOrganizationsRequest<T>(options: BusinessPlatformOrganizationsRequestNonTypedOptions): Promise<T>;
export interface BusinessPlatformOrganizationsRequestOptions<TResult, TVariables extends Variables> extends BusinessPlatformRequestOptions<TResult, TVariables> {
organizationId: string;
}
@@ -54,4 +61,5 @@ export interface BusinessPlatformOrganizationsRequestOptions<TResult, TVariables
* @param options - The options for the request.
* @returns The response of the query of generic type <T>.
*/
-export declare function businessPlatformOrganizationsRequestDoc<TResult, TVariables extends Variables>(options: BusinessPlatformOrganizationsRequestOptions<TResult, TVariables>): Promise<TResult>;
\ No newline at end of file
+declare function businessPlatformOrganizationsRequestDoc<TResult, TVariables extends Variables>(options: BusinessPlatformOrganizationsRequestOptions<TResult, TVariables>): Promise<TResult>;
+export { fetchEmail, businessPlatformRequestDoc, businessPlatformOrganizationsRequest, businessPlatformOrganizationsRequestDoc, };
\ No newline at end of file
packages/cli-kit/dist/public/node/vendor/dev_server/dev-server-2024.d.ts@@ -4,6 +4,7 @@ export declare function createServer(projectName: string): {
url: (options?: HostOptions) => string;
};
declare function assertRunning2024(projectName: string): void;
+export declare function isRunning2024(projectName: string): boolean;
declare let assertRunningOverride: typeof assertRunning2024 | undefined;
export declare function setAssertRunning(override: typeof assertRunningOverride): void;
export {};
\ No newline at end of file
packages/cli-kit/dist/public/node/vendor/dev_server/network/index.d.ts@@ -5,5 +5,4 @@ export interface ConnectionArguments {
port: number;
timeout?: number;
}
-export declare function assertConnectable(options: ConnectionArguments): void;
-export declare function TEST_testResetCheckPort(): void;
\ No newline at end of file
+export declare function assertConnectable(options: ConnectionArguments): void;
\ No newline at end of file
|
WHY are these changes introduced?
Fixes #0000
WHAT is this pull request doing?
How to test your changes?
Post-release steps
Measuring impact
How do we know this change was effective? Please choose one:
Checklist