shopify 3.93.0 → 3.93.2

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 (84) hide show
  1. package/dist/assets/hydrogen/starter/CHANGELOG.md +64 -0
  2. package/dist/assets/hydrogen/starter/app/components/Aside.tsx +4 -2
  3. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +6 -3
  4. package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +117 -23
  5. package/dist/assets/hydrogen/starter/app/components/Header.tsx +3 -3
  6. package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +24 -4
  7. package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +1 -1
  8. package/dist/assets/hydrogen/starter/app/routes/[robots.txt].tsx +14 -53
  9. package/dist/assets/hydrogen/starter/app/routes/_index.tsx +11 -4
  10. package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +1 -1
  11. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +2 -2
  12. package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +1 -1
  13. package/dist/assets/hydrogen/starter/package.json +5 -5
  14. package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +0 -11
  15. package/dist/assets/hydrogen/starter/vite.config.ts +1 -1
  16. package/dist/assets/hydrogen/vite/vite.config.js +1 -1
  17. package/dist/{chunk-DLK7L2KZ.js → chunk-4QL77VYJ.js} +3 -3
  18. package/dist/{chunk-U7JC7ESX.js → chunk-4VZV4LQX.js} +2 -2
  19. package/dist/{chunk-MYTB32VB.js → chunk-5FCKEHCK.js} +177 -181
  20. package/dist/{chunk-POO2TAEO.js → chunk-DDTYWTF2.js} +1 -1
  21. package/dist/{chunk-XV44IQDO.js → chunk-FYQIRCLV.js} +1 -1
  22. package/dist/{chunk-O6CC6JKI.js → chunk-IG47ZDRU.js} +1 -1
  23. package/dist/{chunk-SBSUITP6.js → chunk-MX6WWR5F.js} +1 -1
  24. package/dist/{chunk-P3ASN7B5.js → chunk-XVFYDYZA.js} +1 -1
  25. package/dist/cli/commands/store/auth.d.ts +1 -0
  26. package/dist/cli/commands/store/auth.js +10 -3
  27. package/dist/cli/commands/store/execute.d.ts +1 -0
  28. package/dist/cli/commands/store/execute.js +7 -4
  29. package/dist/cli/services/store/auth/callback.d.ts +8 -0
  30. package/dist/cli/services/store/auth/callback.js +140 -0
  31. package/dist/cli/services/store/{auth-config.js → auth/config.js} +1 -1
  32. package/dist/cli/services/store/auth/existing-scopes.d.ts +5 -0
  33. package/dist/cli/services/store/auth/existing-scopes.js +40 -0
  34. package/dist/cli/services/store/auth/index.d.ts +18 -0
  35. package/dist/cli/services/store/auth/index.js +88 -0
  36. package/dist/cli/services/store/auth/pkce.d.ts +36 -0
  37. package/dist/cli/services/store/auth/pkce.js +49 -0
  38. package/dist/cli/services/store/{auth-recovery.js → auth/recovery.js} +1 -1
  39. package/dist/cli/services/store/auth/result.d.ts +24 -0
  40. package/dist/cli/services/store/auth/result.js +39 -0
  41. package/dist/cli/services/store/auth/scopes.d.ts +4 -0
  42. package/dist/cli/services/store/auth/scopes.js +53 -0
  43. package/dist/cli/services/store/auth/session-lifecycle.d.ts +3 -0
  44. package/dist/cli/services/store/auth/session-lifecycle.js +69 -0
  45. package/dist/cli/services/store/{session.d.ts → auth/session-store.d.ts} +1 -2
  46. package/dist/cli/services/store/auth/session-store.js +127 -0
  47. package/dist/cli/services/store/auth/token-client.d.ts +40 -0
  48. package/dist/cli/services/store/auth/token-client.js +95 -0
  49. package/dist/cli/services/store/{admin-graphql-context.d.ts → execute/admin-context.d.ts} +3 -2
  50. package/dist/cli/services/store/execute/admin-context.js +41 -0
  51. package/dist/cli/services/store/execute/admin-transport.d.ts +6 -0
  52. package/dist/cli/services/store/{admin-graphql-transport.js → execute/admin-transport.js} +7 -7
  53. package/dist/cli/services/store/{execute.d.ts → execute/index.d.ts} +2 -3
  54. package/dist/cli/services/store/{execute.js → execute/index.js} +4 -11
  55. package/dist/cli/services/store/{execute-request.d.ts → execute/request.d.ts} +0 -2
  56. package/dist/cli/services/store/{execute-request.js → execute/request.js} +1 -2
  57. package/dist/cli/services/store/execute/result.d.ts +3 -0
  58. package/dist/cli/services/store/execute/result.js +29 -0
  59. package/dist/cli/services/store/{graphql-targets.d.ts → execute/targets.d.ts} +2 -3
  60. package/dist/cli/services/store/{graphql-targets.js → execute/targets.js} +5 -11
  61. package/dist/{error-handler-IRR4EZPS.js → error-handler-GZ2I7BG5.js} +1 -1
  62. package/dist/hooks/postrun.js +1 -1
  63. package/dist/hooks/prerun.js +1 -1
  64. package/dist/{http-proxy-node16-KBILO6A6.js → http-proxy-node16-DSQMBVDI.js} +1 -1
  65. package/dist/index.js +873 -865
  66. package/dist/{lib-EN3PX6IK.js → lib-GGVLMXY5.js} +1 -1
  67. package/dist/{local-PQUMWHWR.js → local-WHQ3ZS4K.js} +1 -1
  68. package/dist/{node-package-manager-HIOT5VLV.js → node-package-manager-6XMPTNUI.js} +1 -1
  69. package/dist/tsconfig.tsbuildinfo +1 -1
  70. package/dist/{ui-QBSPD4RX.js → ui-GZ7DOSHP.js} +1 -1
  71. package/dist/{workerd-QSZBPNES.js → workerd-LJU6AVMQ.js} +1 -1
  72. package/oclif.manifest.json +24 -5
  73. package/package.json +7 -7
  74. package/dist/assets/hydrogen/starter/app/routes/api.$version.[graphql.json].tsx +0 -14
  75. package/dist/cli/services/store/admin-graphql-context.js +0 -103
  76. package/dist/cli/services/store/admin-graphql-transport.d.ts +0 -9
  77. package/dist/cli/services/store/auth.d.ts +0 -61
  78. package/dist/cli/services/store/auth.js +0 -326
  79. package/dist/cli/services/store/execute-result.d.ts +0 -1
  80. package/dist/cli/services/store/execute-result.js +0 -18
  81. package/dist/cli/services/store/session.js +0 -69
  82. /package/dist/cli/services/store/{auth-config.d.ts → auth/config.d.ts} +0 -0
  83. /package/dist/cli/services/store/{auth-recovery.d.ts → auth/recovery.d.ts} +0 -0
  84. /package/dist/{morph-6DCXNO2H.js → morph-Q32V442A.js} +0 -0
@@ -12,4 +12,4 @@ export function maskToken(token) {
12
12
  return '***';
13
13
  return `${token.slice(0, 10)}***`;
14
14
  }
15
- //# sourceMappingURL=auth-config.js.map
15
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,5 @@
1
+ export interface ResolvedStoreAuthScopes {
2
+ scopes: string[];
3
+ authoritative: boolean;
4
+ }
5
+ export declare function resolveExistingStoreAuthScopes(store: string): Promise<ResolvedStoreAuthScopes>;
@@ -0,0 +1,40 @@
1
+ import { normalizeStoreFqdn } from '@shopify/cli-kit/node/context/fqdn';
2
+ import { outputContent, outputDebug, outputToken } from '@shopify/cli-kit/node/output';
3
+ import { getCurrentStoredStoreAppSession } from './session-store.js';
4
+ import { loadStoredStoreSession } from './session-lifecycle.js';
5
+ import { fetchCurrentStoreAuthScopes } from './token-client.js';
6
+ function truncateDebugMessage(message, length = 300) {
7
+ return message.slice(0, length);
8
+ }
9
+ function formatStoreScopeLookupError(error) {
10
+ if (error && typeof error === 'object' && 'response' in error) {
11
+ const response = error.response;
12
+ const status = response?.status;
13
+ const details = response?.errors;
14
+ if (typeof status === 'number') {
15
+ const summary = typeof details === 'string' ? details : JSON.stringify(details);
16
+ return truncateDebugMessage(summary ? `HTTP ${status}: ${summary}` : `HTTP ${status}`);
17
+ }
18
+ }
19
+ return truncateDebugMessage(error instanceof Error ? error.message : String(error));
20
+ }
21
+ export async function resolveExistingStoreAuthScopes(store) {
22
+ const normalizedStore = normalizeStoreFqdn(store);
23
+ const storedSession = getCurrentStoredStoreAppSession(normalizedStore);
24
+ if (!storedSession)
25
+ return { scopes: [], authoritative: true };
26
+ try {
27
+ const usableSession = await loadStoredStoreSession(normalizedStore);
28
+ const remoteScopes = await fetchCurrentStoreAuthScopes({
29
+ store: usableSession.store,
30
+ accessToken: usableSession.accessToken,
31
+ });
32
+ outputDebug(outputContent `Resolved current remote scopes for ${outputToken.raw(normalizedStore)}: ${outputToken.raw(remoteScopes.join(',') || 'none')}`);
33
+ return { scopes: remoteScopes, authoritative: true };
34
+ }
35
+ catch (error) {
36
+ outputDebug(outputContent `Falling back to locally stored scopes for ${outputToken.raw(normalizedStore)} after remote scope lookup failed: ${outputToken.raw(formatStoreScopeLookupError(error))}`);
37
+ return { scopes: storedSession.scopes, authoritative: false };
38
+ }
39
+ }
40
+ //# sourceMappingURL=existing-scopes.js.map
@@ -0,0 +1,18 @@
1
+ import { openURL } from '@shopify/cli-kit/node/system';
2
+ import { exchangeStoreAuthCodeForToken } from './token-client.js';
3
+ import { waitForStoreAuthCode } from './callback.js';
4
+ import { type ResolvedStoreAuthScopes } from './existing-scopes.js';
5
+ import { type StoreAuthPresenter, type StoreAuthResult } from './result.js';
6
+ interface StoreAuthInput {
7
+ store: string;
8
+ scopes: string;
9
+ }
10
+ interface StoreAuthDependencies {
11
+ openURL: typeof openURL;
12
+ waitForStoreAuthCode: typeof waitForStoreAuthCode;
13
+ exchangeStoreAuthCodeForToken: typeof exchangeStoreAuthCodeForToken;
14
+ resolveExistingScopes: (store: string) => Promise<ResolvedStoreAuthScopes>;
15
+ presenter: StoreAuthPresenter;
16
+ }
17
+ export declare function authenticateStoreWithApp(input: StoreAuthInput, dependencies?: Partial<StoreAuthDependencies>): Promise<StoreAuthResult>;
18
+ export {};
@@ -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 {};