diff --git a/packages/cli-kit/src/private/node/clients/identity/identity-mock-client.ts b/packages/cli-kit/src/private/node/clients/identity/identity-mock-client.ts index 59ff688919..ea109f1df6 100644 --- a/packages/cli-kit/src/private/node/clients/identity/identity-mock-client.ts +++ b/packages/cli-kit/src/private/node/clients/identity/identity-mock-client.ts @@ -3,7 +3,6 @@ import {ApplicationToken, IdentityToken} from '../../session/schema.js' import {ExchangeScopes, TokenRequestResult} from '../../session/exchange.js' import {ok, Result} from '../../../../public/node/result.js' import {allDefaultScopes} from '../../session/scopes.js' -import {applicationId} from '../../session/identity.js' export class IdentityMockClient extends IdentityClient { private readonly mockUserId = '08978734-325e-44ce-bc65-34823a8d5180' @@ -28,11 +27,11 @@ export class IdentityMockClient extends IdentityClient { _store?: string, ): Promise<{[x: string]: ApplicationToken}> { return { - [applicationId('app-management')]: this.generateTokens(applicationId('app-management')), - [applicationId('business-platform')]: this.generateTokens(applicationId('business-platform')), - [applicationId('admin')]: this.generateTokens(applicationId('admin')), - [applicationId('partners')]: this.generateTokens(applicationId('partners')), - [applicationId('storefront-renderer')]: this.generateTokens(applicationId('storefront-renderer')), + [this.applicationId('app-management')]: this.generateTokens(this.applicationId('app-management')), + [this.applicationId('business-platform')]: this.generateTokens(this.applicationId('business-platform')), + [this.applicationId('admin')]: this.generateTokens(this.applicationId('admin')), + [this.applicationId('partners')]: this.generateTokens(this.applicationId('partners')), + [this.applicationId('storefront-renderer')]: this.generateTokens(this.applicationId('storefront-renderer')), } } diff --git a/packages/cli-kit/src/private/node/clients/identity/identity-service-client.ts b/packages/cli-kit/src/private/node/clients/identity/identity-service-client.ts index 88d1a52078..b49eb48f3e 100644 --- a/packages/cli-kit/src/private/node/clients/identity/identity-service-client.ts +++ b/packages/cli-kit/src/private/node/clients/identity/identity-service-client.ts @@ -49,6 +49,9 @@ export class IdentityServiceClient extends IdentityClient { return err({error: payload.error, store: params.store}) } + /** + * Given an expired access token, refresh it to get a new one. + */ async refreshAccessToken(currentToken: IdentityToken): Promise { const clientId = this.clientId() const params = { diff --git a/packages/cli-kit/src/private/node/session.test.ts b/packages/cli-kit/src/private/node/session.test.ts index 533684ac64..e2c9e73ea9 100644 --- a/packages/cli-kit/src/private/node/session.test.ts +++ b/packages/cli-kit/src/private/node/session.test.ts @@ -17,8 +17,6 @@ import {allDefaultScopes} from './session/scopes.js' import {store as storeSessions, fetch as fetchSessions, remove as secureRemove} from './session/store.js' import {ApplicationToken, IdentityToken, Sessions} from './session/schema.js' import {validateSession} from './session/validate.js' -import {applicationId} from './session/identity.js' -import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js' import {getCurrentSessionId} from './conf-store.js' import {getIdentityClient} from './clients/identity/instance.js' import {IdentityMockClient} from './clients/identity/identity-mock-client.js' @@ -129,7 +127,6 @@ beforeEach(() => { vi.spyOn(fqdnModule, 'identityFqdn').mockResolvedValue(fqdn) vi.mocked(exchangeAccessForApplicationTokens).mockResolvedValue(appTokens) vi.mocked(refreshAccessToken).mockResolvedValue(validIdentityToken) - vi.mocked(applicationId).mockImplementation((app) => app) vi.mocked(exchangeCustomPartnerToken).mockResolvedValue({ accessToken: partnersToken.accessToken, userId: validIdentityToken.userId, @@ -139,15 +136,6 @@ beforeEach(() => { setLastSeenUserIdAfterAuth(undefined as any) setLastSeenAuthMethod('none') - vi.mocked(requestDeviceAuthorization).mockResolvedValue({ - deviceCode: 'device_code', - userCode: 'user_code', - verificationUri: 'verification_uri', - expiresIn: 3600, - verificationUriComplete: 'verification_uri_complete', - interval: 5, - }) - vi.mocked(pollForDeviceAuthorization).mockResolvedValue(validIdentityToken) vi.mocked(terminalSupportsPrompting).mockReturnValue(true) vi.mocked(businessPlatformRequest).mockResolvedValue({ currentUserAccount: { @@ -156,6 +144,7 @@ beforeEach(() => { }) vi.mocked(getIdentityClient).mockImplementation(() => mockIdentityClient) + vi.spyOn(mockIdentityClient, 'applicationId').mockImplementation((app) => app) vi.spyOn(mockIdentityClient, 'refreshAccessToken').mockResolvedValue(validIdentityToken) vi.spyOn(mockIdentityClient, 'requestAccessToken').mockResolvedValue(validIdentityToken) }) diff --git a/packages/cli-kit/src/private/node/session.ts b/packages/cli-kit/src/private/node/session.ts index 9b66873d3b..8afb08f5ef 100644 --- a/packages/cli-kit/src/private/node/session.ts +++ b/packages/cli-kit/src/private/node/session.ts @@ -1,4 +1,3 @@ -import {applicationId} from './session/identity.js' import {validateSession} from './session/validate.js' import {allDefaultScopes, apiScopes} from './session/scopes.js' import { @@ -299,13 +298,14 @@ async function executeCompleteFlow(applications: OAuthApplications): Promise { vi.mocked(isTTY).mockReturnValue(true) vi.mocked(isCI).mockReturnValue(false) + vi.mocked(isCloudEnvironment).mockReturnValue(false) + vi.mocked(keypress).mockResolvedValue(undefined) + vi.mocked(openURL).mockResolvedValue(true) + vi.mocked(getIdentityClient).mockImplementation(() => mockIdentityClient) + // Mock stringifyMessage to pass through strings for error messages + vi.mocked(stringifyMessage).mockImplementation((msg) => (typeof msg === 'string' ? msg : String(msg))) }) describe('requestDeviceAuthorization', () => { @@ -32,7 +44,7 @@ describe('requestDeviceAuthorization', () => { verification_uri: 'verification_uri', expires_in: 3600, verification_uri_complete: 'verification_uri_complete', - interval: 5, + interval: 0.05, } const dataExpected: DeviceAuthorizationResponse = { @@ -46,20 +58,31 @@ describe('requestDeviceAuthorization', () => { test('requests an authorization code to initiate the device auth', async () => { // Given - const response = new Response(JSON.stringify(data)) - vi.mocked(shopifyFetch).mockResolvedValue(response) + const deviceAuthResponse = new Response(JSON.stringify(data)) + vi.mocked(shopifyFetch).mockResolvedValueOnce(deviceAuthResponse) vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') + // Mock the token exchange to complete the flow + const identityToken: IdentityToken = { + accessToken: 'access_token', + refreshToken: 'refresh_token', + expiresAt: new Date(2022, 1, 1, 11), + scopes: ['scope1', 'scope2'], + userId: '1234-5678', + alias: '1234-5678', + } + vi.mocked(exchangeDeviceCodeForAccessToken).mockResolvedValue(ok(identityToken)) + // When - const got = await requestDeviceAuthorization(['scope1', 'scope2']) + const got = await mockIdentityClient.requestAccessToken(['scope1', 'scope2']) // Then - expect(shopifyFetch).toBeCalledWith('https://fqdn.com/oauth/device_authorization', { + expect(shopifyFetch).toHaveBeenCalledWith('https://fqdn.com/oauth/device_authorization', { method: 'POST', headers: {'Content-type': 'application/x-www-form-urlencoded'}, body: 'client_id=fbdb2649-e327-4907-8f67-908d24cfd7e3&scope=scope1 scope2', }) - expect(got).toEqual(dataExpected) + expect(got).toEqual(identityToken) }) test('when the response is not valid JSON, throw an error with context', async () => { @@ -71,7 +94,7 @@ describe('requestDeviceAuthorization', () => { vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') // When/Then - await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError( + await expect(mockIdentityClient.requestAccessToken(['scope1', 'scope2'])).rejects.toThrowError( 'Received invalid response from authorization service (HTTP 200). Response could not be parsed as valid JSON. If this issue persists, please contact support at https://help.shopify.com', ) }) @@ -85,7 +108,7 @@ describe('requestDeviceAuthorization', () => { vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') // When/Then - await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError( + await expect(mockIdentityClient.requestAccessToken(['scope1', 'scope2'])).rejects.toThrowError( 'Received invalid response from authorization service (HTTP 200). Received empty response body. If this issue persists, please contact support at https://help.shopify.com', ) }) @@ -100,7 +123,7 @@ describe('requestDeviceAuthorization', () => { vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') // When/Then - await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError( + await expect(mockIdentityClient.requestAccessToken(['scope1', 'scope2'])).rejects.toThrowError( 'Received invalid response from authorization service (HTTP 404). The request may be malformed or unauthorized. Received HTML instead of JSON - the service endpoint may have changed. If this issue persists, please contact support at https://help.shopify.com', ) }) @@ -114,7 +137,7 @@ describe('requestDeviceAuthorization', () => { vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') // When/Then - await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError( + await expect(mockIdentityClient.requestAccessToken(['scope1', 'scope2'])).rejects.toThrowError( 'Received invalid response from authorization service (HTTP 500). The service may be experiencing issues. Response could not be parsed as valid JSON. If this issue persists, please contact support at https://help.shopify.com', ) }) @@ -130,13 +153,23 @@ describe('requestDeviceAuthorization', () => { vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') // When/Then - await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError( + await expect(mockIdentityClient.requestAccessToken(['scope1', 'scope2'])).rejects.toThrowError( 'Failed to read response from authorization service (HTTP 200). Network or streaming error occurred.', ) }) }) describe('pollForDeviceAuthorization', () => { + const data: any = { + device_code: 'device_code', + user_code: 'user_code', + verification_uri: 'verification_uri', + expires_in: 3600, + verification_uri_complete: 'verification_uri_complete', + // Short interval for testing + interval: 0.05, + } + const identityToken: IdentityToken = { accessToken: 'access_token', refreshToken: 'refresh_token', @@ -148,13 +181,16 @@ describe('pollForDeviceAuthorization', () => { test('poll until a valid token is received', async () => { // Given + const deviceAuthResponse = new Response(JSON.stringify(data)) + vi.mocked(shopifyFetch).mockResolvedValueOnce(deviceAuthResponse) + vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') vi.mocked(exchangeDeviceCodeForAccessToken).mockResolvedValueOnce(err('authorization_pending')) vi.mocked(exchangeDeviceCodeForAccessToken).mockResolvedValueOnce(err('authorization_pending')) vi.mocked(exchangeDeviceCodeForAccessToken).mockResolvedValueOnce(err('authorization_pending')) vi.mocked(exchangeDeviceCodeForAccessToken).mockResolvedValueOnce(ok(identityToken)) // When - const got = await pollForDeviceAuthorization('device_code', 0.05) + const got = await mockIdentityClient.requestAccessToken(['scope1', 'scope2']) // Then expect(exchangeDeviceCodeForAccessToken).toBeCalledTimes(4) @@ -163,12 +199,15 @@ describe('pollForDeviceAuthorization', () => { test('when polling, if an error is received, stop polling and throw error', async () => { // Given + const deviceAuthResponse = new Response(JSON.stringify(data)) + vi.mocked(shopifyFetch).mockResolvedValueOnce(deviceAuthResponse) + vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') vi.mocked(exchangeDeviceCodeForAccessToken).mockResolvedValueOnce(err('authorization_pending')) vi.mocked(exchangeDeviceCodeForAccessToken).mockResolvedValueOnce(err('authorization_pending')) vi.mocked(exchangeDeviceCodeForAccessToken).mockResolvedValueOnce(err('access_denied')) // When - const got = pollForDeviceAuthorization('device_code', 0.05) + const got = mockIdentityClient.requestAccessToken(['scope1', 'scope2']) // Then await expect(got).rejects.toThrow() diff --git a/packages/cli-kit/src/private/node/session/device-authorization.ts b/packages/cli-kit/src/private/node/session/device-authorization.ts index 7353862920..85ec5f8ec3 100644 --- a/packages/cli-kit/src/private/node/session/device-authorization.ts +++ b/packages/cli-kit/src/private/node/session/device-authorization.ts @@ -1,13 +1,3 @@ -import {exchangeDeviceCodeForAccessToken} from './exchange.js' -import {IdentityToken} from './schema.js' -import {identityFqdn} from '../../../public/node/context/fqdn.js' -import {shopifyFetch} from '../../../public/node/http.js' -import {outputContent, outputDebug, outputInfo, outputToken} from '../../../public/node/output.js' -import {AbortError, BugError} from '../../../public/node/error.js' -import {isCloudEnvironment} from '../../../public/node/context/local.js' -import {isCI, openURL} from '../../../public/node/system.js' -import {isTTY, keypress} from '../../../public/node/ui.js' -import {getIdentityClient} from '../clients/identity/instance.js' import {Response} from 'node-fetch' export interface DeviceAuthorizationResponse { @@ -19,145 +9,6 @@ export interface DeviceAuthorizationResponse { interval?: number } -/** - * 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. - */ -export async function requestDeviceAuthorization(scopes: string[]): Promise { - const fqdn = await identityFqdn() - const identityClientId = getIdentityClient().clientId() - const queryParams = {client_id: identityClientId, scope: scopes.join(' ')} - const url = `https://${fqdn}/oauth/device_authorization` - - const response = await shopifyFetch(url, { - method: 'POST', - headers: {'Content-type': 'application/x-www-form-urlencoded'}, - body: convertRequestToParams(queryParams), - }) - - // First read the response body as text so we have it for debugging - let responseText: string - try { - responseText = await response.text() - } catch (error) { - throw new BugError( - `Failed to read response from authorization service (HTTP ${response.status}). Network or streaming error occurred.`, - 'Check your network connection and try again.', - ) - } - - // Now try to parse the text as JSON - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let jsonResult: any - try { - jsonResult = JSON.parse(responseText) - } catch { - // JSON.parse failed, handle the parsing error - const errorMessage = buildAuthorizationParseErrorMessage(response, responseText) - throw new BugError(errorMessage) - } - - outputDebug(outputContent`Received device authorization code: ${outputToken.json(jsonResult)}`) - if (!jsonResult.device_code || !jsonResult.verification_uri_complete) { - throw new BugError('Failed to start authorization process') - } - - outputInfo('\nTo run this command, log in to Shopify.') - - if (isCI()) { - throw new AbortError( - 'Authorization is required to continue, but the current environment does not support interactive prompts.', - 'To resolve this, specify credentials in your environment, or run the command in an interactive environment such as your local terminal.', - ) - } - - outputInfo(outputContent`User verification code: ${jsonResult.user_code}`) - const linkToken = outputToken.link(jsonResult.verification_uri_complete) - - const cloudMessage = () => { - outputInfo(outputContent`👉 Open this link to start the auth process: ${linkToken}`) - } - - if (isCloudEnvironment() || !isTTY()) { - cloudMessage() - } else { - outputInfo('👉 Press any key to open the login page on your browser') - await keypress() - const opened = await openURL(jsonResult.verification_uri_complete) - if (opened) { - outputInfo(outputContent`Opened link to start the auth process: ${linkToken}`) - } else { - cloudMessage() - } - } - - return { - deviceCode: jsonResult.device_code, - userCode: jsonResult.user_code, - verificationUri: jsonResult.verification_uri, - expiresIn: jsonResult.expires_in, - verificationUriComplete: jsonResult.verification_uri_complete, - interval: jsonResult.interval, - } -} - -/** - * 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 code - The device code obtained after starting a device identity flow - * @param interval - The interval to poll the token endpoint - * @returns The identity token - */ -export async function pollForDeviceAuthorization(code: string, interval = 5): Promise { - let currentIntervalInSeconds = interval - - return new Promise((resolve, reject) => { - const onPoll = async () => { - const result = await exchangeDeviceCodeForAccessToken(code) - if (!result.isErr()) { - resolve(result.value) - return - } - - const error = result.error ?? 'unknown_failure' - - outputDebug(outputContent`Polling for device authorization... status: ${error}`) - switch (error) { - case 'authorization_pending': { - startPolling() - return - } - case 'slow_down': - currentIntervalInSeconds += 5 - startPolling() - return - case 'access_denied': - case 'expired_token': - case 'unknown_failure': { - reject(new Error(`Device authorization failed: ${error}`)) - } - } - } - - const startPolling = () => { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - setTimeout(onPoll, currentIntervalInSeconds * 1000) - } - - startPolling() - }) -} - export function convertRequestToParams(queryParams: {client_id: string; scope: string}): string { return Object.entries(queryParams) .map(([key, value]) => value && `${key}=${value}`) diff --git a/packages/cli-kit/src/private/node/session/exchange.test.ts b/packages/cli-kit/src/private/node/session/exchange.test.ts index f900d3e14f..df0bd50fca 100644 --- a/packages/cli-kit/src/private/node/session/exchange.test.ts +++ b/packages/cli-kit/src/private/node/session/exchange.test.ts @@ -8,12 +8,13 @@ import { refreshAccessToken, requestAppToken, } from './exchange.js' -import {applicationId} from './identity.js' import {IdentityToken} from './schema.js' import {shopifyFetch} from '../../../public/node/http.js' import {identityFqdn} from '../../../public/node/context/fqdn.js' import {getLastSeenUserIdAfterAuth, getLastSeenAuthMethod} from '../session.js' import {AbortError} from '../../../public/node/error.js' +import {getIdentityClient} from '../clients/identity/instance.js' +import {IdentityServiceClient} from '../clients/identity/identity-service-client.js' import {describe, test, expect, vi, afterAll, beforeEach} from 'vitest' import {Response} from 'node-fetch' @@ -25,7 +26,6 @@ const data: any = { refresh_token: 'refresh_token', scope: 'scope scope2', expires_in: 3600, - // id_token:{sub: '1234-5678'} id_token: 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0LTU2NzgifQ.L8IiNHncR4xe42f1fLQZFD5D_HBo7oMlfop2FS-NUCU', } @@ -38,14 +38,18 @@ const identityToken: IdentityToken = { alias: '1234-5678', } +// use real client since we stub out network requests in this "integration" test +const mockIdentityClient = new IdentityServiceClient() + vi.mock('../../../public/node/http.js') vi.mock('../../../public/node/context/fqdn.js') -vi.mock('./identity') +vi.mock('../clients/identity/instance.js') beforeEach(() => { vi.setSystemTime(currentDate) - vi.mocked(applicationId).mockImplementation((api) => api) vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') + vi.mocked(getIdentityClient).mockImplementation(() => mockIdentityClient) + vi.spyOn(mockIdentityClient, 'applicationId').mockImplementation((api) => api) }) afterAll(() => { diff --git a/packages/cli-kit/src/private/node/session/exchange.ts b/packages/cli-kit/src/private/node/session/exchange.ts index 7d815bea09..395cc11f5d 100644 --- a/packages/cli-kit/src/private/node/session/exchange.ts +++ b/packages/cli-kit/src/private/node/session/exchange.ts @@ -1,9 +1,6 @@ import {ApplicationToken, IdentityToken} from './schema.js' -import {applicationId} from './identity.js' import {tokenExchangeScopes} from './scopes.js' import {API} from '../api.js' -import {identityFqdn} from '../../../public/node/context/fqdn.js' -import {shopifyFetch} from '../../../public/node/http.js' import {err, ok, Result} from '../../../public/node/result.js' import {AbortError, BugError, ExtendableError} from '../../../public/node/error.js' import {setLastSeenAuthMethod, setLastSeenUserIdAfterAuth} from '../session.js' @@ -53,9 +50,6 @@ export async function exchangeAccessForApplicationTokens( } } -/** - * Given an expired access token, refresh it to get a new one. - */ export async function refreshAccessToken(currentToken: IdentityToken): Promise { const clientId = getIdentityClient().clientId() const params = { @@ -81,7 +75,7 @@ async function exchangeCliTokenForAccessToken( token: string, scopes: string[], ): Promise<{accessToken: string; userId: string}> { - const appId = applicationId(apiName) + const appId = getIdentityClient().applicationId(apiName) try { const newToken = await requestAppToken(apiName, token, scopes) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -164,14 +158,14 @@ export async function requestAppToken( scopes: string[] = [], store?: string, ): Promise<{[x: string]: ApplicationToken}> { - const appId = applicationId(api) - const clientId = getIdentityClient().clientId() + const identityClient = getIdentityClient() + const appId = identityClient.applicationId(api) const params = { grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', requested_token_type: 'urn:ietf:params:oauth:token-type:access_token', subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', - client_id: clientId, + client_id: identityClient.clientId(), audience: appId, scope: scopes.join(' '), subject_token: token, @@ -222,22 +216,6 @@ export function tokenRequestErrorHandler({error, store}: {error: string; store?: return new AbortError(error) } -async function _tokenRequest(params: { - [key: string]: string -}): Promise> { - const fqdn = await identityFqdn() - const url = new URL(`https://${fqdn}/oauth/token`) - url.search = new URLSearchParams(Object.entries(params)).toString() - - const res = await shopifyFetch(url.href, {method: 'POST'}) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = await res.json() - - if (res.ok) return ok(payload) - - return err({error: payload.error, store: params.store}) -} - export function buildIdentityToken( result: TokenRequestResult, existingUserId?: string, diff --git a/packages/cli-kit/src/private/node/session/identity.ts b/packages/cli-kit/src/private/node/session/identity.ts deleted file mode 100644 index 81a8efd404..0000000000 --- a/packages/cli-kit/src/private/node/session/identity.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {API} from '../api.js' -import {BugError} from '../../../public/node/error.js' -import {Environment, serviceEnvironment} from '../context/service.js' - -function _clientId(): string { - const environment = serviceEnvironment() - if (environment === Environment.Local) { - return 'e5380e02-312a-7408-5718-e07017e9cf52' - } else if (environment === Environment.Production) { - return 'fbdb2649-e327-4907-8f67-908d24cfd7e3' - } else { - return 'e5380e02-312a-7408-5718-e07017e9cf52' - } -} - -export function applicationId(api: API): string { - switch (api) { - case 'admin': { - const environment = serviceEnvironment() - if (environment === Environment.Local) { - return 'e92482cebb9bfb9fb5a0199cc770fde3de6c8d16b798ee73e36c9d815e070e52' - } else if (environment === Environment.Production) { - return '7ee65a63608843c577db8b23c4d7316ea0a01bd2f7594f8a9c06ea668c1b775c' - } else { - return 'e92482cebb9bfb9fb5a0199cc770fde3de6c8d16b798ee73e36c9d815e070e52' - } - } - case 'partners': { - const environment = serviceEnvironment() - if (environment === Environment.Local) { - return 'df89d73339ac3c6c5f0a98d9ca93260763e384d51d6038da129889c308973978' - } else if (environment === Environment.Production) { - return '271e16d403dfa18082ffb3d197bd2b5f4479c3fc32736d69296829cbb28d41a6' - } else { - return 'df89d73339ac3c6c5f0a98d9ca93260763e384d51d6038da129889c308973978' - } - } - case 'storefront-renderer': { - const environment = serviceEnvironment() - if (environment === Environment.Local) { - return '46f603de-894f-488d-9471-5b721280ff49' - } else if (environment === Environment.Production) { - return 'ee139b3d-5861-4d45-b387-1bc3ada7811c' - } else { - return '46f603de-894f-488d-9471-5b721280ff49' - } - } - case 'business-platform': { - const environment = serviceEnvironment() - if (environment === Environment.Local) { - return 'ace6dc89-b526-456d-a942-4b8ef6acda4b' - } else if (environment === Environment.Production) { - return '32ff8ee5-82b8-4d93-9f8a-c6997cefb7dc' - } else { - return 'ace6dc89-b526-456d-a942-4b8ef6acda4b' - } - } - case 'app-management': { - const environment = serviceEnvironment() - if (environment === Environment.Production) { - return '7ee65a63608843c577db8b23c4d7316ea0a01bd2f7594f8a9c06ea668c1b775c' - } else { - return 'e92482cebb9bfb9fb5a0199cc770fde3de6c8d16b798ee73e36c9d815e070e52' - } - } - default: - throw new BugError(`Application id for API of type: ${api}`) - } -} diff --git a/packages/cli-kit/src/private/node/session/validate.test.ts b/packages/cli-kit/src/private/node/session/validate.test.ts index 12f8300b68..e8545c185b 100644 --- a/packages/cli-kit/src/private/node/session/validate.test.ts +++ b/packages/cli-kit/src/private/node/session/validate.test.ts @@ -1,7 +1,8 @@ import {validateSession} from './validate.js' -import {applicationId} from './identity.js' import {IdentityToken, validateCachedIdentityTokenStructure} from './schema.js' import {OAuthApplications} from '../session.js' +import {getIdentityClient} from '../clients/identity/instance.js' +import {IdentityMockClient} from '../clients/identity/identity-mock-client.js' import {expect, describe, test, vi, afterAll, beforeEach} from 'vitest' const pastDate = new Date(2022, 1, 1, 9) @@ -70,14 +71,18 @@ const defaultApps: OAuthApplications = { storefrontRendererApi: {scopes: []}, } +const mockIdentityClient = new IdentityMockClient() + vi.mock('./identity-token-validation') vi.mock('./identity') vi.mock('./schema') +vi.mock('../clients/identity/instance.js') beforeEach(() => { - vi.mocked(applicationId).mockImplementation((id: any) => id) vi.mocked(validateCachedIdentityTokenStructure).mockReturnValue(true) vi.setSystemTime(currentDate) + vi.mocked(getIdentityClient).mockImplementation(() => mockIdentityClient) + vi.spyOn(mockIdentityClient, 'applicationId').mockImplementation((api) => api) }) afterAll(() => { diff --git a/packages/cli-kit/src/private/node/session/validate.ts b/packages/cli-kit/src/private/node/session/validate.ts index 2dd97f2480..af80813e1d 100644 --- a/packages/cli-kit/src/private/node/session/validate.ts +++ b/packages/cli-kit/src/private/node/session/validate.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import {applicationId} from './identity.js' import {ApplicationToken, IdentityToken, Session, validateCachedIdentityTokenStructure} from './schema.js' import {sessionConstants} from '../constants.js' import {firstPartyDev} from '../../../public/node/context/local.js' import {OAuthApplications} from '../session.js' import {outputDebug} from '../../../public/node/output.js' +import {getIdentityClient} from '../clients/identity/instance.js' type ValidationResult = 'needs_refresh' | 'needs_full_auth' | 'ok' @@ -33,27 +33,28 @@ export async function validateSession( const scopesAreValid = validateScopes(scopes, session.identity) if (!scopesAreValid) return 'needs_full_auth' let tokensAreExpired = isTokenExpired(session.identity) + const identityClient = getIdentityClient() if (applications.partnersApi) { - const appId = applicationId('partners') + const appId = identityClient.applicationId('partners') const token = session.applications[appId]! tokensAreExpired = tokensAreExpired || isTokenExpired(token) } if (applications.appManagementApi) { - const appId = applicationId('app-management') + const appId = identityClient.applicationId('app-management') const token = session.applications[appId]! tokensAreExpired = tokensAreExpired || isTokenExpired(token) } if (applications.storefrontRendererApi) { - const appId = applicationId('storefront-renderer') + const appId = identityClient.applicationId('storefront-renderer') const token = session.applications[appId]! tokensAreExpired = tokensAreExpired || isTokenExpired(token) } if (applications.adminApi) { - const appId = applicationId('admin') + const appId = identityClient.applicationId('admin') const realAppId = `${applications.adminApi.storeFqdn}-${appId}` const token = session.applications[realAppId]! tokensAreExpired = tokensAreExpired || isTokenExpired(token)