shopify 3.93.0 → 3.93.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/{chunk-SBSUITP6.js → chunk-PB3UDYWH.js} +1 -1
  2. package/dist/{chunk-MYTB32VB.js → chunk-SVYSLNQH.js} +1 -1
  3. package/dist/{chunk-O6CC6JKI.js → chunk-T57REQVZ.js} +1 -1
  4. package/dist/{chunk-U7JC7ESX.js → chunk-TCRHJ3ZH.js} +2 -2
  5. package/dist/{chunk-POO2TAEO.js → chunk-VLDSGLBP.js} +1 -1
  6. package/dist/{chunk-DLK7L2KZ.js → chunk-WOERFYNW.js} +2 -2
  7. package/dist/cli/commands/store/auth.d.ts +1 -0
  8. package/dist/cli/commands/store/auth.js +10 -3
  9. package/dist/cli/commands/store/execute.d.ts +1 -0
  10. package/dist/cli/commands/store/execute.js +7 -4
  11. package/dist/cli/services/store/auth/callback.d.ts +8 -0
  12. package/dist/cli/services/store/auth/callback.js +140 -0
  13. package/dist/cli/services/store/{auth-config.js → auth/config.js} +1 -1
  14. package/dist/cli/services/store/auth/existing-scopes.d.ts +5 -0
  15. package/dist/cli/services/store/auth/existing-scopes.js +40 -0
  16. package/dist/cli/services/store/auth/index.d.ts +18 -0
  17. package/dist/cli/services/store/auth/index.js +88 -0
  18. package/dist/cli/services/store/auth/pkce.d.ts +36 -0
  19. package/dist/cli/services/store/auth/pkce.js +49 -0
  20. package/dist/cli/services/store/{auth-recovery.js → auth/recovery.js} +1 -1
  21. package/dist/cli/services/store/auth/result.d.ts +24 -0
  22. package/dist/cli/services/store/auth/result.js +39 -0
  23. package/dist/cli/services/store/auth/scopes.d.ts +4 -0
  24. package/dist/cli/services/store/auth/scopes.js +53 -0
  25. package/dist/cli/services/store/auth/session-lifecycle.d.ts +3 -0
  26. package/dist/cli/services/store/auth/session-lifecycle.js +69 -0
  27. package/dist/cli/services/store/{session.d.ts → auth/session-store.d.ts} +1 -2
  28. package/dist/cli/services/store/auth/session-store.js +127 -0
  29. package/dist/cli/services/store/auth/token-client.d.ts +40 -0
  30. package/dist/cli/services/store/auth/token-client.js +95 -0
  31. package/dist/cli/services/store/{admin-graphql-context.d.ts → execute/admin-context.d.ts} +3 -2
  32. package/dist/cli/services/store/execute/admin-context.js +41 -0
  33. package/dist/cli/services/store/execute/admin-transport.d.ts +6 -0
  34. package/dist/cli/services/store/{admin-graphql-transport.js → execute/admin-transport.js} +7 -7
  35. package/dist/cli/services/store/{execute.d.ts → execute/index.d.ts} +2 -3
  36. package/dist/cli/services/store/{execute.js → execute/index.js} +4 -11
  37. package/dist/cli/services/store/{execute-request.d.ts → execute/request.d.ts} +0 -2
  38. package/dist/cli/services/store/{execute-request.js → execute/request.js} +1 -2
  39. package/dist/cli/services/store/execute/result.d.ts +3 -0
  40. package/dist/cli/services/store/execute/result.js +29 -0
  41. package/dist/cli/services/store/{graphql-targets.d.ts → execute/targets.d.ts} +2 -3
  42. package/dist/cli/services/store/{graphql-targets.js → execute/targets.js} +5 -11
  43. package/dist/{error-handler-IRR4EZPS.js → error-handler-54XVSWV5.js} +1 -1
  44. package/dist/hooks/postrun.js +1 -1
  45. package/dist/hooks/prerun.js +1 -1
  46. package/dist/index.js +867 -859
  47. package/dist/{local-PQUMWHWR.js → local-JCUIPKND.js} +1 -1
  48. package/dist/{node-package-manager-HIOT5VLV.js → node-package-manager-P7JQBCHZ.js} +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/dist/{ui-QBSPD4RX.js → ui-XTVYPIVY.js} +1 -1
  51. package/dist/{workerd-QSZBPNES.js → workerd-3GJRSBJN.js} +1 -1
  52. package/oclif.manifest.json +23 -3
  53. package/package.json +6 -6
  54. package/dist/cli/services/store/admin-graphql-context.js +0 -103
  55. package/dist/cli/services/store/admin-graphql-transport.d.ts +0 -9
  56. package/dist/cli/services/store/auth.d.ts +0 -61
  57. package/dist/cli/services/store/auth.js +0 -326
  58. package/dist/cli/services/store/execute-result.d.ts +0 -1
  59. package/dist/cli/services/store/execute-result.js +0 -18
  60. package/dist/cli/services/store/session.js +0 -69
  61. /package/dist/cli/services/store/{auth-config.d.ts → auth/config.d.ts} +0 -0
  62. /package/dist/cli/services/store/{auth-recovery.d.ts → auth/recovery.d.ts} +0 -0
@@ -0,0 +1,88 @@
1
+ import { normalizeStoreFqdn } from '@shopify/cli-kit/node/context/fqdn';
2
+ import { AbortError } from '@shopify/cli-kit/node/error';
3
+ import { outputContent, outputDebug, outputToken } from '@shopify/cli-kit/node/output';
4
+ import { openURL } from '@shopify/cli-kit/node/system';
5
+ import { STORE_AUTH_APP_CLIENT_ID } from './config.js';
6
+ import { setStoredStoreAppSession } from './session-store.js';
7
+ import { exchangeStoreAuthCodeForToken } from './token-client.js';
8
+ import { waitForStoreAuthCode } from './callback.js';
9
+ import { createPkceBootstrap } from './pkce.js';
10
+ import { mergeRequestedAndStoredScopes, parseStoreAuthScopes, resolveGrantedScopes } from './scopes.js';
11
+ import { resolveExistingStoreAuthScopes } from './existing-scopes.js';
12
+ import { createStoreAuthPresenter } from './result.js';
13
+ const defaultStoreAuthDependencies = {
14
+ openURL,
15
+ waitForStoreAuthCode,
16
+ exchangeStoreAuthCodeForToken,
17
+ resolveExistingScopes: resolveExistingStoreAuthScopes,
18
+ presenter: createStoreAuthPresenter('text'),
19
+ };
20
+ export async function authenticateStoreWithApp(input, dependencies = {}) {
21
+ const resolvedDependencies = { ...defaultStoreAuthDependencies, ...dependencies };
22
+ const store = normalizeStoreFqdn(input.store);
23
+ const requestedScopes = parseStoreAuthScopes(input.scopes);
24
+ const existingScopeResolution = await resolvedDependencies.resolveExistingScopes(store);
25
+ const scopes = mergeRequestedAndStoredScopes(requestedScopes, existingScopeResolution.scopes);
26
+ const validationScopes = existingScopeResolution.authoritative ? scopes : requestedScopes;
27
+ if (existingScopeResolution.scopes.length > 0) {
28
+ outputDebug(outputContent `Merged requested scopes ${outputToken.raw(requestedScopes.join(','))} with existing scopes ${outputToken.raw(existingScopeResolution.scopes.join(','))} for ${outputToken.raw(store)}`);
29
+ }
30
+ const bootstrap = createPkceBootstrap({
31
+ store,
32
+ scopes,
33
+ exchangeCodeForToken: resolvedDependencies.exchangeStoreAuthCodeForToken,
34
+ });
35
+ const { authorization: { authorizationUrl }, } = bootstrap;
36
+ resolvedDependencies.presenter.openingBrowser();
37
+ const code = await resolvedDependencies.waitForStoreAuthCode({
38
+ ...bootstrap.waitForAuthCodeOptions,
39
+ onListening: async () => {
40
+ const opened = await resolvedDependencies.openURL(authorizationUrl);
41
+ if (!opened)
42
+ resolvedDependencies.presenter.manualAuthUrl(authorizationUrl);
43
+ },
44
+ });
45
+ const tokenResponse = await bootstrap.exchangeCodeForToken(code);
46
+ const userId = tokenResponse.associated_user?.id?.toString();
47
+ if (!userId) {
48
+ throw new AbortError('Shopify did not return associated user information for the online access token.');
49
+ }
50
+ const now = Date.now();
51
+ const expiresAt = tokenResponse.expires_in ? new Date(now + tokenResponse.expires_in * 1000).toISOString() : undefined;
52
+ const result = {
53
+ store,
54
+ userId,
55
+ scopes: resolveGrantedScopes(tokenResponse, validationScopes),
56
+ acquiredAt: new Date(now).toISOString(),
57
+ expiresAt,
58
+ refreshTokenExpiresAt: tokenResponse.refresh_token_expires_in
59
+ ? new Date(now + tokenResponse.refresh_token_expires_in * 1000).toISOString()
60
+ : undefined,
61
+ hasRefreshToken: !!tokenResponse.refresh_token,
62
+ associatedUser: tokenResponse.associated_user
63
+ ? {
64
+ id: tokenResponse.associated_user.id,
65
+ email: tokenResponse.associated_user.email,
66
+ firstName: tokenResponse.associated_user.first_name,
67
+ lastName: tokenResponse.associated_user.last_name,
68
+ accountOwner: tokenResponse.associated_user.account_owner,
69
+ }
70
+ : undefined,
71
+ };
72
+ setStoredStoreAppSession({
73
+ store,
74
+ clientId: STORE_AUTH_APP_CLIENT_ID,
75
+ userId,
76
+ accessToken: tokenResponse.access_token,
77
+ refreshToken: tokenResponse.refresh_token,
78
+ scopes: result.scopes,
79
+ acquiredAt: result.acquiredAt,
80
+ expiresAt,
81
+ refreshTokenExpiresAt: result.refreshTokenExpiresAt,
82
+ associatedUser: result.associatedUser,
83
+ });
84
+ outputDebug(outputContent `Session persisted for ${outputToken.raw(store)} (user ${outputToken.raw(userId)}, expires ${outputToken.raw(expiresAt ?? 'unknown')})`);
85
+ resolvedDependencies.presenter.success(result);
86
+ return result;
87
+ }
88
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,36 @@
1
+ import type { StoreTokenResponse } from './token-client.js';
2
+ import type { WaitForAuthCodeOptions } from './callback.js';
3
+ interface StoreAuthorizationContext {
4
+ store: string;
5
+ scopes: string[];
6
+ state: string;
7
+ port: number;
8
+ redirectUri: string;
9
+ authorizationUrl: string;
10
+ codeVerifier: string;
11
+ }
12
+ interface StoreAuthBootstrap {
13
+ authorization: StoreAuthorizationContext;
14
+ waitForAuthCodeOptions: WaitForAuthCodeOptions;
15
+ exchangeCodeForToken: (code: string) => Promise<StoreTokenResponse>;
16
+ }
17
+ export declare function generateCodeVerifier(): string;
18
+ export declare function computeCodeChallenge(verifier: string): string;
19
+ export declare function buildStoreAuthUrl(options: {
20
+ store: string;
21
+ scopes: string[];
22
+ state: string;
23
+ redirectUri: string;
24
+ codeChallenge: string;
25
+ }): string;
26
+ export declare function createPkceBootstrap(options: {
27
+ store: string;
28
+ scopes: string[];
29
+ exchangeCodeForToken: (options: {
30
+ store: string;
31
+ code: string;
32
+ codeVerifier: string;
33
+ redirectUri: string;
34
+ }) => Promise<StoreTokenResponse>;
35
+ }): StoreAuthBootstrap;
36
+ export {};
@@ -0,0 +1,49 @@
1
+ import { randomUUID } from '@shopify/cli-kit/node/crypto';
2
+ import { outputContent, outputDebug, outputToken } from '@shopify/cli-kit/node/output';
3
+ import { createHash, randomBytes } from 'crypto';
4
+ import { DEFAULT_STORE_AUTH_PORT, STORE_AUTH_APP_CLIENT_ID, storeAuthRedirectUri } from './config.js';
5
+ export function generateCodeVerifier() {
6
+ return randomBytes(32).toString('base64url');
7
+ }
8
+ export function computeCodeChallenge(verifier) {
9
+ return createHash('sha256').update(verifier).digest('base64url');
10
+ }
11
+ export function buildStoreAuthUrl(options) {
12
+ const params = new URLSearchParams();
13
+ params.set('client_id', STORE_AUTH_APP_CLIENT_ID);
14
+ params.set('scope', options.scopes.join(','));
15
+ params.set('redirect_uri', options.redirectUri);
16
+ params.set('state', options.state);
17
+ params.set('response_type', 'code');
18
+ params.set('code_challenge', options.codeChallenge);
19
+ params.set('code_challenge_method', 'S256');
20
+ return `https://${options.store}/admin/oauth/authorize?${params.toString()}`;
21
+ }
22
+ export function createPkceBootstrap(options) {
23
+ const { store, scopes, exchangeCodeForToken } = options;
24
+ const port = DEFAULT_STORE_AUTH_PORT;
25
+ const state = randomUUID();
26
+ const redirectUri = storeAuthRedirectUri(port);
27
+ const codeVerifier = generateCodeVerifier();
28
+ const codeChallenge = computeCodeChallenge(codeVerifier);
29
+ const authorizationUrl = buildStoreAuthUrl({ store, scopes, state, redirectUri, codeChallenge });
30
+ outputDebug(outputContent `Starting PKCE auth for ${outputToken.raw(store)} with scopes ${outputToken.raw(scopes.join(','))} (redirect_uri=${outputToken.raw(redirectUri)})`);
31
+ return {
32
+ authorization: {
33
+ store,
34
+ scopes,
35
+ state,
36
+ port,
37
+ redirectUri,
38
+ authorizationUrl,
39
+ codeVerifier,
40
+ },
41
+ waitForAuthCodeOptions: {
42
+ store,
43
+ state,
44
+ port,
45
+ },
46
+ exchangeCodeForToken: (code) => exchangeCodeForToken({ store, code, codeVerifier, redirectUri }),
47
+ };
48
+ }
49
+ //# sourceMappingURL=pkce.js.map
@@ -14,4 +14,4 @@ export function reauthenticateStoreAuthError(message, store, scopes) {
14
14
  export function retryStoreAuthWithPermanentDomainError(returnedStore) {
15
15
  return new AbortError('OAuth callback store does not match the requested store.', `Shopify returned ${returnedStore} during authentication. Re-run using the permanent store domain:`, storeAuthCommandNextSteps(returnedStore, '<comma-separated-scopes>'));
16
16
  }
17
- //# sourceMappingURL=auth-recovery.js.map
17
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1,24 @@
1
+ export interface StoreAuthResult {
2
+ store: string;
3
+ userId: string;
4
+ scopes: string[];
5
+ acquiredAt: string;
6
+ expiresAt?: string;
7
+ refreshTokenExpiresAt?: string;
8
+ hasRefreshToken: boolean;
9
+ associatedUser?: {
10
+ id: number;
11
+ email?: string;
12
+ firstName?: string;
13
+ lastName?: string;
14
+ accountOwner?: boolean;
15
+ };
16
+ }
17
+ type StoreAuthOutputFormat = 'text' | 'json';
18
+ export interface StoreAuthPresenter {
19
+ openingBrowser: () => void;
20
+ manualAuthUrl: (authorizationUrl: string) => void;
21
+ success: (result: StoreAuthResult) => void;
22
+ }
23
+ export declare function createStoreAuthPresenter(format?: StoreAuthOutputFormat): StoreAuthPresenter;
24
+ export {};
@@ -0,0 +1,39 @@
1
+ import { outputCompleted, outputInfo, outputResult, outputToken, outputContent } from '@shopify/cli-kit/node/output';
2
+ function serializeStoreAuthResult(result) {
3
+ return JSON.stringify(result, null, 2);
4
+ }
5
+ function buildStoreAuthSuccessText(result) {
6
+ const displayName = result.associatedUser?.email ? ` as ${result.associatedUser.email}` : '';
7
+ return {
8
+ completed: ['Logged in.', `Authenticated${displayName} against ${result.store}.`],
9
+ info: ['', 'To verify that authentication worked, run:', `shopify store execute --store ${result.store} --query 'query { shop { name id } }'`],
10
+ };
11
+ }
12
+ function displayStoreAuthOpeningBrowser() {
13
+ outputInfo('Shopify CLI will open the app authorization page in your browser.');
14
+ outputInfo('');
15
+ }
16
+ function displayStoreAuthManualAuthUrl(authorizationUrl) {
17
+ outputInfo('Browser did not open automatically. Open this URL manually:');
18
+ outputInfo(outputContent `${outputToken.link(authorizationUrl)}`);
19
+ outputInfo('');
20
+ }
21
+ function displayStoreAuthResult(result, format = 'text') {
22
+ if (format === 'json') {
23
+ outputResult(serializeStoreAuthResult(result));
24
+ return;
25
+ }
26
+ const text = buildStoreAuthSuccessText(result);
27
+ text.completed.forEach((line) => outputCompleted(line));
28
+ text.info.forEach((line) => outputInfo(line));
29
+ }
30
+ export function createStoreAuthPresenter(format = 'text') {
31
+ return {
32
+ openingBrowser: displayStoreAuthOpeningBrowser,
33
+ manualAuthUrl: displayStoreAuthManualAuthUrl,
34
+ success(result) {
35
+ displayStoreAuthResult(result, format);
36
+ },
37
+ };
38
+ }
39
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1,4 @@
1
+ import type { StoreTokenResponse } from './token-client.js';
2
+ export declare function parseStoreAuthScopes(input: string): string[];
3
+ export declare function mergeRequestedAndStoredScopes(requestedScopes: string[], storedScopes: string[]): string[];
4
+ export declare function resolveGrantedScopes(tokenResponse: StoreTokenResponse, requestedScopes: string[]): string[];
@@ -0,0 +1,53 @@
1
+ import { AbortError } from '@shopify/cli-kit/node/error';
2
+ import { outputContent, outputDebug } from '@shopify/cli-kit/node/output';
3
+ export function parseStoreAuthScopes(input) {
4
+ const scopes = input
5
+ .split(',')
6
+ .map((scope) => scope.trim())
7
+ .filter(Boolean);
8
+ if (scopes.length === 0) {
9
+ throw new AbortError('At least one scope is required.', 'Pass --scopes as a comma-separated list.');
10
+ }
11
+ return [...new Set(scopes)];
12
+ }
13
+ function expandImpliedStoreScopes(scopes) {
14
+ const expandedScopes = new Set(scopes);
15
+ for (const scope of scopes) {
16
+ const matches = scope.match(/^(unauthenticated_)?write_(.*)$/);
17
+ if (matches) {
18
+ expandedScopes.add(`${matches[1] ?? ''}read_${matches[2]}`);
19
+ }
20
+ }
21
+ return expandedScopes;
22
+ }
23
+ export function mergeRequestedAndStoredScopes(requestedScopes, storedScopes) {
24
+ const mergedScopes = [...storedScopes];
25
+ const expandedScopes = expandImpliedStoreScopes(storedScopes);
26
+ for (const scope of requestedScopes) {
27
+ if (expandedScopes.has(scope))
28
+ continue;
29
+ mergedScopes.push(scope);
30
+ for (const expandedScope of expandImpliedStoreScopes([scope])) {
31
+ expandedScopes.add(expandedScope);
32
+ }
33
+ }
34
+ return mergedScopes;
35
+ }
36
+ export function resolveGrantedScopes(tokenResponse, requestedScopes) {
37
+ if (!tokenResponse.scope) {
38
+ outputDebug(outputContent `Token response did not include scope; falling back to requested scopes`);
39
+ return requestedScopes;
40
+ }
41
+ const grantedScopes = parseStoreAuthScopes(tokenResponse.scope);
42
+ const expandedGrantedScopes = expandImpliedStoreScopes(grantedScopes);
43
+ const missingScopes = requestedScopes.filter((scope) => !expandedGrantedScopes.has(scope));
44
+ if (missingScopes.length > 0) {
45
+ throw new AbortError('Shopify granted fewer scopes than were requested.', `Missing scopes: ${missingScopes.join(', ')}.`, [
46
+ 'Update the app or store installation scopes.',
47
+ 'See https://shopify.dev/app/scopes',
48
+ 'Re-run shopify store auth.',
49
+ ]);
50
+ }
51
+ return grantedScopes;
52
+ }
53
+ //# sourceMappingURL=scopes.js.map
@@ -0,0 +1,3 @@
1
+ import type { StoredStoreAppSession } from './session-store.js';
2
+ export declare function isSessionExpired(session: StoredStoreAppSession): boolean;
3
+ export declare function loadStoredStoreSession(store: string): Promise<StoredStoreAppSession>;
@@ -0,0 +1,69 @@
1
+ import { AbortError } from '@shopify/cli-kit/node/error';
2
+ import { outputContent, outputDebug, outputToken } from '@shopify/cli-kit/node/output';
3
+ import { maskToken } from './config.js';
4
+ import { createStoredStoreAuthError, reauthenticateStoreAuthError } from './recovery.js';
5
+ import { clearStoredStoreAppSession, getCurrentStoredStoreAppSession, setStoredStoreAppSession, } from './session-store.js';
6
+ import { refreshStoreAccessToken } from './token-client.js';
7
+ const EXPIRY_MARGIN_MS = 4 * 60 * 1000;
8
+ export function isSessionExpired(session) {
9
+ if (!session.expiresAt)
10
+ return false;
11
+ const expiresAtMs = new Date(session.expiresAt).getTime();
12
+ if (Number.isNaN(expiresAtMs))
13
+ return true;
14
+ return expiresAtMs - EXPIRY_MARGIN_MS < Date.now();
15
+ }
16
+ function buildRefreshedStoredSession(session, refresh) {
17
+ const now = Date.now();
18
+ const expiresAt = refresh.expiresIn ? new Date(now + refresh.expiresIn * 1000).toISOString() : session.expiresAt;
19
+ return {
20
+ ...session,
21
+ accessToken: refresh.accessToken,
22
+ refreshToken: refresh.refreshToken ?? session.refreshToken,
23
+ expiresAt,
24
+ refreshTokenExpiresAt: refresh.refreshTokenExpiresIn
25
+ ? new Date(now + refresh.refreshTokenExpiresIn * 1000).toISOString()
26
+ : session.refreshTokenExpiresAt,
27
+ acquiredAt: new Date(now).toISOString(),
28
+ };
29
+ }
30
+ export async function loadStoredStoreSession(store) {
31
+ let session = getCurrentStoredStoreAppSession(store);
32
+ if (!session) {
33
+ throw createStoredStoreAuthError(store);
34
+ }
35
+ outputDebug(outputContent `Loaded stored session for ${outputToken.raw(store)}: token=${outputToken.raw(maskToken(session.accessToken))}, expires=${outputToken.raw(session.expiresAt ?? 'unknown')}`);
36
+ if (!isSessionExpired(session)) {
37
+ return session;
38
+ }
39
+ if (!session.refreshToken) {
40
+ throw reauthenticateStoreAuthError(`No refresh token stored for ${session.store}.`, session.store, session.scopes.join(','));
41
+ }
42
+ outputDebug(outputContent `Refreshing expired token for ${outputToken.raw(session.store)} (expired at ${outputToken.raw(session.expiresAt ?? 'unknown')}, refresh_token=${outputToken.raw(maskToken(session.refreshToken))})`);
43
+ const previousAccessToken = session.accessToken;
44
+ let refreshed;
45
+ try {
46
+ refreshed = await refreshStoreAccessToken({
47
+ store: session.store,
48
+ refreshToken: session.refreshToken,
49
+ });
50
+ }
51
+ catch (error) {
52
+ clearStoredStoreAppSession(session.store, session.userId);
53
+ if (error instanceof AbortError && error.message.startsWith(`Token refresh failed for ${session.store} (HTTP `)) {
54
+ throw reauthenticateStoreAuthError(error.message, session.store, session.scopes.join(','));
55
+ }
56
+ if (error instanceof AbortError && error.message === `Token refresh returned an invalid response for ${session.store}.`) {
57
+ throw reauthenticateStoreAuthError(error.message, session.store, session.scopes.join(','));
58
+ }
59
+ if (error instanceof AbortError && error.message === 'Received an invalid refresh response from Shopify.') {
60
+ throw error;
61
+ }
62
+ throw error;
63
+ }
64
+ session = buildRefreshedStoredSession(session, refreshed);
65
+ outputDebug(outputContent `Token refresh succeeded for ${outputToken.raw(session.store)}: ${outputToken.raw(maskToken(previousAccessToken))} → ${outputToken.raw(maskToken(session.accessToken))}, new expiry ${outputToken.raw(session.expiresAt ?? 'unknown')}`);
66
+ setStoredStoreAppSession(session);
67
+ return session;
68
+ }
69
+ //# sourceMappingURL=session-lifecycle.js.map
@@ -26,8 +26,7 @@ interface StoredStoreAppSessionBucket {
26
26
  interface StoreSessionSchema {
27
27
  [key: string]: StoredStoreAppSessionBucket;
28
28
  }
29
- export declare function getStoredStoreAppSession(store: string, storage?: LocalStorage<StoreSessionSchema>): StoredStoreAppSession | undefined;
29
+ export declare function getCurrentStoredStoreAppSession(store: string, storage?: LocalStorage<StoreSessionSchema>): StoredStoreAppSession | undefined;
30
30
  export declare function setStoredStoreAppSession(session: StoredStoreAppSession, storage?: LocalStorage<StoreSessionSchema>): void;
31
31
  export declare function clearStoredStoreAppSession(store: string, userIdOrStorage?: string | LocalStorage<StoreSessionSchema>, maybeStorage?: LocalStorage<StoreSessionSchema>): void;
32
- export declare function isSessionExpired(session: StoredStoreAppSession): boolean;
33
32
  export {};
@@ -0,0 +1,127 @@
1
+ import { LocalStorage } from '@shopify/cli-kit/node/local-storage';
2
+ import { storeAuthSessionKey } from './config.js';
3
+ let _storeSessionStorage;
4
+ function storeSessionStorage() {
5
+ _storeSessionStorage ?? (_storeSessionStorage = new LocalStorage({ projectName: 'shopify-cli-store' }));
6
+ return _storeSessionStorage;
7
+ }
8
+ function isString(value) {
9
+ return typeof value === 'string';
10
+ }
11
+ function sanitizeAssociatedUser(value) {
12
+ if (!value || typeof value !== 'object')
13
+ return undefined;
14
+ const associatedUser = value;
15
+ if (typeof associatedUser.id !== 'number')
16
+ return undefined;
17
+ return {
18
+ id: associatedUser.id,
19
+ ...(isString(associatedUser.email) ? { email: associatedUser.email } : {}),
20
+ ...(isString(associatedUser.firstName) ? { firstName: associatedUser.firstName } : {}),
21
+ ...(isString(associatedUser.lastName) ? { lastName: associatedUser.lastName } : {}),
22
+ ...(typeof associatedUser.accountOwner === 'boolean' ? { accountOwner: associatedUser.accountOwner } : {}),
23
+ };
24
+ }
25
+ function sanitizeStoredStoreAppSession(value) {
26
+ if (!value || typeof value !== 'object')
27
+ return undefined;
28
+ const session = value;
29
+ if (!isString(session.store) ||
30
+ !isString(session.clientId) ||
31
+ !isString(session.userId) ||
32
+ !isString(session.accessToken) ||
33
+ !Array.isArray(session.scopes) ||
34
+ !session.scopes.every(isString) ||
35
+ !isString(session.acquiredAt)) {
36
+ return undefined;
37
+ }
38
+ return {
39
+ store: session.store,
40
+ clientId: session.clientId,
41
+ userId: session.userId,
42
+ accessToken: session.accessToken,
43
+ scopes: session.scopes,
44
+ acquiredAt: session.acquiredAt,
45
+ ...(isString(session.refreshToken) ? { refreshToken: session.refreshToken } : {}),
46
+ ...(isString(session.expiresAt) ? { expiresAt: session.expiresAt } : {}),
47
+ ...(isString(session.refreshTokenExpiresAt) ? { refreshTokenExpiresAt: session.refreshTokenExpiresAt } : {}),
48
+ ...(sanitizeAssociatedUser(session.associatedUser) ? { associatedUser: sanitizeAssociatedUser(session.associatedUser) } : {}),
49
+ };
50
+ }
51
+ function readStoredStoreAppSessionBucket(store, storage) {
52
+ const key = storeAuthSessionKey(store);
53
+ const storedBucket = storage.get(key);
54
+ if (!storedBucket || typeof storedBucket !== 'object')
55
+ return undefined;
56
+ const { sessionsByUserId, currentUserId } = storedBucket;
57
+ if (!sessionsByUserId || typeof sessionsByUserId !== 'object' || Array.isArray(sessionsByUserId) || typeof currentUserId !== 'string') {
58
+ storage.delete(key);
59
+ return undefined;
60
+ }
61
+ const sanitizedSessionsByUserId = Object.fromEntries(Object.entries(sessionsByUserId).flatMap(([userId, session]) => {
62
+ const sanitizedSession = sanitizeStoredStoreAppSession(session);
63
+ return sanitizedSession ? [[userId, sanitizedSession]] : [];
64
+ }));
65
+ if (Object.keys(sanitizedSessionsByUserId).length !== Object.keys(sessionsByUserId).length) {
66
+ if (sanitizedSessionsByUserId[currentUserId]) {
67
+ storage.set(key, {
68
+ currentUserId,
69
+ sessionsByUserId: sanitizedSessionsByUserId,
70
+ });
71
+ }
72
+ else {
73
+ storage.delete(key);
74
+ return undefined;
75
+ }
76
+ }
77
+ return {
78
+ currentUserId,
79
+ sessionsByUserId: sanitizedSessionsByUserId,
80
+ };
81
+ }
82
+ export function getCurrentStoredStoreAppSession(store, storage = storeSessionStorage()) {
83
+ const bucket = readStoredStoreAppSessionBucket(store, storage);
84
+ if (!bucket)
85
+ return undefined;
86
+ const session = bucket.sessionsByUserId[bucket.currentUserId];
87
+ if (!session) {
88
+ storage.delete(storeAuthSessionKey(store));
89
+ return undefined;
90
+ }
91
+ return session;
92
+ }
93
+ export function setStoredStoreAppSession(session, storage = storeSessionStorage()) {
94
+ const key = storeAuthSessionKey(session.store);
95
+ const existingBucket = readStoredStoreAppSessionBucket(session.store, storage);
96
+ const nextBucket = {
97
+ currentUserId: session.userId,
98
+ sessionsByUserId: {
99
+ ...(existingBucket?.sessionsByUserId ?? {}),
100
+ [session.userId]: session,
101
+ },
102
+ };
103
+ storage.set(key, nextBucket);
104
+ }
105
+ export function clearStoredStoreAppSession(store, userIdOrStorage, maybeStorage) {
106
+ const userId = typeof userIdOrStorage === 'string' ? userIdOrStorage : undefined;
107
+ const storage = (typeof userIdOrStorage === 'string' ? maybeStorage : userIdOrStorage) ?? storeSessionStorage();
108
+ const key = storeAuthSessionKey(store);
109
+ if (!userId) {
110
+ storage.delete(key);
111
+ return;
112
+ }
113
+ const existingBucket = readStoredStoreAppSessionBucket(store, storage);
114
+ if (!existingBucket)
115
+ return;
116
+ const { [userId]: _removedSession, ...remainingSessions } = existingBucket.sessionsByUserId;
117
+ const remainingUserIds = Object.keys(remainingSessions);
118
+ if (remainingUserIds.length === 0) {
119
+ storage.delete(key);
120
+ return;
121
+ }
122
+ storage.set(key, {
123
+ currentUserId: existingBucket.currentUserId === userId ? remainingUserIds[0] : existingBucket.currentUserId,
124
+ sessionsByUserId: remainingSessions,
125
+ });
126
+ }
127
+ //# sourceMappingURL=session-store.js.map
@@ -0,0 +1,40 @@
1
+ export interface StoreTokenResponse {
2
+ access_token: string;
3
+ token_type?: string;
4
+ scope?: string;
5
+ expires_in?: number;
6
+ refresh_token?: string;
7
+ refresh_token_expires_in?: number;
8
+ associated_user_scope?: string;
9
+ associated_user?: {
10
+ id: number;
11
+ first_name?: string;
12
+ last_name?: string;
13
+ email?: string;
14
+ account_owner?: boolean;
15
+ locale?: string;
16
+ collaborator?: boolean;
17
+ email_verified?: boolean;
18
+ };
19
+ }
20
+ interface StoreTokenRefreshPayload {
21
+ accessToken: string;
22
+ refreshToken?: string;
23
+ expiresIn?: number;
24
+ refreshTokenExpiresIn?: number;
25
+ }
26
+ export declare function exchangeStoreAuthCodeForToken(options: {
27
+ store: string;
28
+ code: string;
29
+ codeVerifier: string;
30
+ redirectUri: string;
31
+ }): Promise<StoreTokenResponse>;
32
+ export declare function refreshStoreAccessToken(options: {
33
+ store: string;
34
+ refreshToken: string;
35
+ }): Promise<StoreTokenRefreshPayload>;
36
+ export declare function fetchCurrentStoreAuthScopes(options: {
37
+ store: string;
38
+ accessToken: string;
39
+ }): Promise<string[]>;
40
+ export {};
@@ -0,0 +1,95 @@
1
+ import { adminUrl } from '@shopify/cli-kit/node/api/admin';
2
+ import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
3
+ import { AbortError } from '@shopify/cli-kit/node/error';
4
+ import { fetch } from '@shopify/cli-kit/node/http';
5
+ import { outputContent, outputDebug, outputToken } from '@shopify/cli-kit/node/output';
6
+ import { maskToken, STORE_AUTH_APP_CLIENT_ID } from './config.js';
7
+ function truncateHttpErrorBody(body, length = 300) {
8
+ return body.slice(0, length);
9
+ }
10
+ export async function exchangeStoreAuthCodeForToken(options) {
11
+ const endpoint = `https://${options.store}/admin/oauth/access_token`;
12
+ outputDebug(outputContent `Exchanging authorization code for token at ${outputToken.raw(endpoint)}`);
13
+ const response = await fetch(endpoint, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({
17
+ client_id: STORE_AUTH_APP_CLIENT_ID,
18
+ code: options.code,
19
+ code_verifier: options.codeVerifier,
20
+ redirect_uri: options.redirectUri,
21
+ }),
22
+ });
23
+ const body = await response.text();
24
+ if (!response.ok) {
25
+ outputDebug(outputContent `Token exchange failed with HTTP ${outputToken.raw(String(response.status))}: ${outputToken.raw(truncateHttpErrorBody(body || response.statusText))}`);
26
+ throw new AbortError(`Failed to exchange OAuth code for an access token (HTTP ${response.status}).`, body || response.statusText);
27
+ }
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(body);
31
+ }
32
+ catch {
33
+ throw new AbortError('Received an invalid token response from Shopify.');
34
+ }
35
+ outputDebug(outputContent `Token exchange succeeded: access_token=${outputToken.raw(maskToken(parsed.access_token))}, refresh_token=${outputToken.raw(parsed.refresh_token ? maskToken(parsed.refresh_token) : 'none')}, expires_in=${outputToken.raw(String(parsed.expires_in ?? 'unknown'))}s, user=${outputToken.raw(String(parsed.associated_user?.id ?? 'unknown'))} (${outputToken.raw(parsed.associated_user?.email ?? 'no email')})`);
36
+ return parsed;
37
+ }
38
+ export async function refreshStoreAccessToken(options) {
39
+ const endpoint = `https://${options.store}/admin/oauth/access_token`;
40
+ outputDebug(outputContent `Refreshing access token for ${outputToken.raw(options.store)} using refresh_token=${outputToken.raw(maskToken(options.refreshToken))}`);
41
+ const response = await fetch(endpoint, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({
45
+ client_id: STORE_AUTH_APP_CLIENT_ID,
46
+ grant_type: 'refresh_token',
47
+ refresh_token: options.refreshToken,
48
+ }),
49
+ });
50
+ const body = await response.text();
51
+ if (!response.ok) {
52
+ outputDebug(outputContent `Token refresh failed with HTTP ${outputToken.raw(String(response.status))}: ${outputToken.raw(truncateHttpErrorBody(body || response.statusText))}`);
53
+ throw new AbortError(`Token refresh failed for ${options.store} (HTTP ${response.status}).`);
54
+ }
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(body);
58
+ }
59
+ catch {
60
+ throw new AbortError('Received an invalid refresh response from Shopify.');
61
+ }
62
+ if (!parsed.access_token) {
63
+ throw new AbortError(`Token refresh returned an invalid response for ${options.store}.`);
64
+ }
65
+ return {
66
+ accessToken: parsed.access_token,
67
+ refreshToken: parsed.refresh_token,
68
+ expiresIn: parsed.expires_in,
69
+ refreshTokenExpiresIn: parsed.refresh_token_expires_in,
70
+ };
71
+ }
72
+ const CurrentAppInstallationAccessScopesQuery = `#graphql
73
+ query CurrentAppInstallationAccessScopes {
74
+ currentAppInstallation {
75
+ accessScopes {
76
+ handle
77
+ }
78
+ }
79
+ }
80
+ `;
81
+ export async function fetchCurrentStoreAuthScopes(options) {
82
+ outputDebug(outputContent `Fetching current app installation scopes for ${outputToken.raw(options.store)} using token ${outputToken.raw(maskToken(options.accessToken))}`);
83
+ const data = await graphqlRequest({
84
+ query: CurrentAppInstallationAccessScopesQuery,
85
+ api: 'Admin',
86
+ url: adminUrl(options.store, 'unstable'),
87
+ token: options.accessToken,
88
+ responseOptions: { handleErrors: false },
89
+ });
90
+ if (!Array.isArray(data.currentAppInstallation?.accessScopes)) {
91
+ throw new Error('Shopify did not return currentAppInstallation.accessScopes.');
92
+ }
93
+ return data.currentAppInstallation.accessScopes.flatMap((scope) => typeof scope.handle === 'string' ? [scope.handle] : []);
94
+ }
95
+ //# sourceMappingURL=token-client.js.map
@@ -1,8 +1,9 @@
1
- import { AdminSession } from '@shopify/cli-kit/node/session';
1
+ import type { AdminSession } from '@shopify/cli-kit/node/session';
2
+ import type { StoredStoreAppSession } from '../auth/session-store.js';
2
3
  export interface AdminStoreGraphQLContext {
3
4
  adminSession: AdminSession;
4
5
  version: string;
5
- sessionUserId: string;
6
+ session: StoredStoreAppSession;
6
7
  }
7
8
  export declare function prepareAdminStoreGraphQLContext(input: {
8
9
  store: string;