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
@@ -1 +1 @@
1
- import{Ac as l,Bc as m,Cc as n,Dc as o,Ec as p,Fc as q,Gc as r,ec as a,fc as b,rc as c,sc as d,tc as e,uc as f,vc as g,wc as h,xc as i,yc as j,zc as k}from"./chunk-DLK7L2KZ.js";import"./chunk-P3ASN7B5.js";import"./chunk-XV44IQDO.js";import"./chunk-QJEBL3WX.js";import"./chunk-EENHXSWU.js";import"./chunk-NOSKVZWJ.js";import"./chunk-T4M5CWAO.js";import"./chunk-PRVQAHWI.js";import"./chunk-QUTQDXSL.js";import"./chunk-VPRTJUIN.js";export{b as handleCtrlC,r as isTTY,q as keypress,a as render,k as renderAutocompletePrompt,c as renderConcurrent,j as renderConfirmationPrompt,p as renderDangerousConfirmationPrompt,g as renderError,h as renderFatalError,d as renderInfo,i as renderSelectPrompt,n as renderSingleTask,e as renderSuccess,l as renderTable,m as renderTasks,o as renderTextPrompt,f as renderWarning};
1
+ import{Ac as l,Bc as m,Cc as n,Dc as o,Ec as p,Fc as q,Gc as r,ec as a,fc as b,rc as c,sc as d,tc as e,uc as f,vc as g,wc as h,xc as i,yc as j,zc as k}from"./chunk-WOERFYNW.js";import"./chunk-P3ASN7B5.js";import"./chunk-XV44IQDO.js";import"./chunk-QJEBL3WX.js";import"./chunk-EENHXSWU.js";import"./chunk-NOSKVZWJ.js";import"./chunk-T4M5CWAO.js";import"./chunk-PRVQAHWI.js";import"./chunk-QUTQDXSL.js";import"./chunk-VPRTJUIN.js";export{b as handleCtrlC,r as isTTY,q as keypress,a as render,k as renderAutocompletePrompt,c as renderConcurrent,j as renderConfirmationPrompt,p as renderDangerousConfirmationPrompt,g as renderError,h as renderFatalError,d as renderInfo,i as renderSelectPrompt,n as renderSingleTask,e as renderSuccess,l as renderTable,m as renderTasks,o as renderTextPrompt,f as renderWarning};
@@ -1,4 +1,4 @@
1
- import{Ua as b,Va as k,Wa as E,X as v,Xa as A,Ya as I,Za as L,_a as x,mb as s,ob as B,pb as O}from"./chunk-MYTB32VB.js";import"./chunk-KADPE3PY.js";import"./chunk-O6CC6JKI.js";import"./chunk-U7JC7ESX.js";import"./chunk-PRKBO42R.js";import"./chunk-SBSUITP6.js";import{Gd as q,Jc as R,Qa as g,ic as N,tc as w,va as y}from"./chunk-DLK7L2KZ.js";import"./chunk-P3ASN7B5.js";import"./chunk-XV44IQDO.js";import"./chunk-QJEBL3WX.js";import"./chunk-EENHXSWU.js";import{d as h,g as f}from"./chunk-NOSKVZWJ.js";import"./chunk-T4M5CWAO.js";import"./chunk-PRVQAHWI.js";import"./chunk-QUTQDXSL.js";import{g as p}from"./chunk-VPRTJUIN.js";p();import{createRequire as C}from"node:module";async function ee({root:c,appPort:D,inspectorPort:P,assetsPort:S,debug:U=!1,watch:W=!1,buildPathWorkerFile:$,buildPathClient:M,env:l}){let{createMiniOxygen:F,Response:u}=await v("@shopify/mini-oxygen",c).catch(L);k({Response:u});async function _(){let r=C(import.meta.url).resolve("@shopify/hydrogen/customer-account.schema.json");return new u(g(r),{headers:{"Content-Type":"application/json"}})}let t="hydrogen",o=h(c,$),m=()=>y(o).catch(e=>{throw new R(`Could not read worker file.
1
+ import{Ua as b,Va as k,Wa as E,X as v,Xa as A,Ya as I,Za as L,_a as x,mb as s,ob as B,pb as O}from"./chunk-SVYSLNQH.js";import"./chunk-KADPE3PY.js";import"./chunk-T57REQVZ.js";import"./chunk-TCRHJ3ZH.js";import"./chunk-PRKBO42R.js";import"./chunk-PB3UDYWH.js";import{Gd as q,Jc as R,Qa as g,ic as N,tc as w,va as y}from"./chunk-WOERFYNW.js";import"./chunk-P3ASN7B5.js";import"./chunk-XV44IQDO.js";import"./chunk-QJEBL3WX.js";import"./chunk-EENHXSWU.js";import{d as h,g as f}from"./chunk-NOSKVZWJ.js";import"./chunk-T4M5CWAO.js";import"./chunk-PRVQAHWI.js";import"./chunk-QUTQDXSL.js";import{g as p}from"./chunk-VPRTJUIN.js";p();import{createRequire as C}from"node:module";async function ee({root:c,appPort:D,inspectorPort:P,assetsPort:S,debug:U=!1,watch:W=!1,buildPathWorkerFile:$,buildPathClient:M,env:l}){let{createMiniOxygen:F,Response:u}=await v("@shopify/mini-oxygen",c).catch(L);k({Response:u});async function _(){let r=C(import.meta.url).resolve("@shopify/hydrogen/customer-account.schema.json");return new u(g(r),{headers:{"Content-Type":"application/json"}})}let t="hydrogen",o=h(c,$),m=()=>y(o).catch(e=>{throw new R(`Could not read worker file.
2
2
 
3
3
  `+e.stack,"Did you build the project?")}),i=F({debug:U,port:D,host:"localhost",liveReload:W,requestHook:x,inspectorPort:P,inspectWorkerName:t,assets:{port:S,directory:M},workers:[{name:"hydrogen:middleware",modules:!0,script:`export default { fetch: (request, env) => {
4
4
  const url = new URL(request.url);
@@ -5743,9 +5743,19 @@
5743
5743
  "descriptionWithMarkdown": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.",
5744
5744
  "enableJsonFlag": false,
5745
5745
  "examples": [
5746
- "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products"
5746
+ "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products",
5747
+ "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products --json"
5747
5748
  ],
5748
5749
  "flags": {
5750
+ "json": {
5751
+ "allowNo": false,
5752
+ "char": "j",
5753
+ "description": "Output the result as JSON. Automatically disables color output.",
5754
+ "env": "SHOPIFY_FLAG_JSON",
5755
+ "hidden": false,
5756
+ "name": "json",
5757
+ "type": "boolean"
5758
+ },
5749
5759
  "no-color": {
5750
5760
  "allowNo": false,
5751
5761
  "description": "Disable color output.",
@@ -5803,7 +5813,8 @@
5803
5813
  "examples": [
5804
5814
  "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"query { shop { name } }\"",
5805
5815
  "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query-file ./operation.graphql --variables '{\"id\":\"gid://shopify/Product/1\"}'",
5806
- "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"mutation { shop { id } }\" --allow-mutations"
5816
+ "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"mutation { shop { id } }\" --allow-mutations",
5817
+ "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"query { shop { name } }\" --json"
5807
5818
  ],
5808
5819
  "flags": {
5809
5820
  "allow-mutations": {
@@ -5813,6 +5824,15 @@
5813
5824
  "name": "allow-mutations",
5814
5825
  "type": "boolean"
5815
5826
  },
5827
+ "json": {
5828
+ "allowNo": false,
5829
+ "char": "j",
5830
+ "description": "Output the result as JSON. Automatically disables color output.",
5831
+ "env": "SHOPIFY_FLAG_JSON",
5832
+ "hidden": false,
5833
+ "name": "json",
5834
+ "type": "boolean"
5835
+ },
5816
5836
  "no-color": {
5817
5837
  "allowNo": false,
5818
5838
  "description": "Disable color output.",
@@ -8380,5 +8400,5 @@
8380
8400
  "summary": "Trigger delivery of a sample webhook topic payload to a designated address."
8381
8401
  }
8382
8402
  },
8383
- "version": "3.93.0"
8403
+ "version": "3.93.1"
8384
8404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify",
3
- "version": "3.93.0",
3
+ "version": "3.93.1",
4
4
  "private": false,
5
5
  "description": "A CLI tool to build for the Shopify platform",
6
6
  "keywords": [
@@ -49,11 +49,11 @@
49
49
  "@oclif/core": "4.5.3",
50
50
  "@oclif/plugin-commands": "4.1.33",
51
51
  "@oclif/plugin-plugins": "5.4.47",
52
- "@shopify/app": "3.93.0",
53
- "@shopify/cli-kit": "3.93.0",
54
- "@shopify/plugin-cloudflare": "3.93.0",
55
- "@shopify/plugin-did-you-mean": "3.93.0",
56
- "@shopify/theme": "3.93.0",
52
+ "@shopify/app": "3.93.1",
53
+ "@shopify/cli-kit": "3.93.1",
54
+ "@shopify/plugin-cloudflare": "3.93.1",
55
+ "@shopify/plugin-did-you-mean": "3.93.1",
56
+ "@shopify/theme": "3.93.1",
57
57
  "@shopify/cli-hydrogen": "11.1.10",
58
58
  "@types/global-agent": "3.0.0",
59
59
  "@vitest/coverage-istanbul": "^3.1.4",
@@ -1,103 +0,0 @@
1
- import { fetchApiVersions } from '@shopify/cli-kit/node/api/admin';
2
- import { AbortError } from '@shopify/cli-kit/node/error';
3
- import { fetch } from '@shopify/cli-kit/node/http';
4
- import { outputContent, outputDebug, outputToken } from '@shopify/cli-kit/node/output';
5
- import { maskToken, STORE_AUTH_APP_CLIENT_ID } from './auth-config.js';
6
- import { createStoredStoreAuthError, reauthenticateStoreAuthError } from './auth-recovery.js';
7
- import { clearStoredStoreAppSession, getStoredStoreAppSession, isSessionExpired, setStoredStoreAppSession, } from './session.js';
8
- async function refreshStoreToken(session) {
9
- if (!session.refreshToken) {
10
- throw reauthenticateStoreAuthError(`No refresh token stored for ${session.store}.`, session.store, session.scopes.join(','));
11
- }
12
- const endpoint = `https://${session.store}/admin/oauth/access_token`;
13
- outputDebug(outputContent `Refreshing expired token for ${outputToken.raw(session.store)} (expired at ${outputToken.raw(session.expiresAt ?? 'unknown')}, refresh_token=${outputToken.raw(maskToken(session.refreshToken))})`);
14
- const response = await fetch(endpoint, {
15
- method: 'POST',
16
- headers: { 'Content-Type': 'application/json' },
17
- body: JSON.stringify({
18
- client_id: STORE_AUTH_APP_CLIENT_ID,
19
- grant_type: 'refresh_token',
20
- refresh_token: session.refreshToken,
21
- }),
22
- });
23
- const body = await response.text();
24
- if (!response.ok) {
25
- outputDebug(outputContent `Token refresh failed with HTTP ${outputToken.raw(String(response.status))}: ${outputToken.raw(body.slice(0, 300))}`);
26
- clearStoredStoreAppSession(session.store, session.userId);
27
- throw reauthenticateStoreAuthError(`Token refresh failed for ${session.store} (HTTP ${response.status}).`, session.store, session.scopes.join(','));
28
- }
29
- let data;
30
- try {
31
- data = JSON.parse(body);
32
- }
33
- catch {
34
- clearStoredStoreAppSession(session.store, session.userId);
35
- throw new AbortError('Received an invalid refresh response from Shopify.');
36
- }
37
- if (!data.access_token) {
38
- clearStoredStoreAppSession(session.store, session.userId);
39
- throw reauthenticateStoreAuthError(`Token refresh returned an invalid response for ${session.store}.`, session.store, session.scopes.join(','));
40
- }
41
- const now = Date.now();
42
- const expiresAt = data.expires_in ? new Date(now + data.expires_in * 1000).toISOString() : session.expiresAt;
43
- const refreshedSession = {
44
- ...session,
45
- accessToken: data.access_token,
46
- refreshToken: data.refresh_token ?? session.refreshToken,
47
- expiresAt,
48
- refreshTokenExpiresAt: data.refresh_token_expires_in
49
- ? new Date(now + data.refresh_token_expires_in * 1000).toISOString()
50
- : session.refreshTokenExpiresAt,
51
- acquiredAt: new Date(now).toISOString(),
52
- };
53
- outputDebug(outputContent `Token refresh succeeded for ${outputToken.raw(session.store)}: ${outputToken.raw(maskToken(session.accessToken))} → ${outputToken.raw(maskToken(refreshedSession.accessToken))}, new expiry ${outputToken.raw(expiresAt ?? 'unknown')}`);
54
- setStoredStoreAppSession(refreshedSession);
55
- return refreshedSession;
56
- }
57
- async function loadStoredStoreSession(store) {
58
- let session = getStoredStoreAppSession(store);
59
- if (!session) {
60
- throw createStoredStoreAuthError(store);
61
- }
62
- outputDebug(outputContent `Loaded stored session for ${outputToken.raw(store)}: token=${outputToken.raw(maskToken(session.accessToken))}, expires=${outputToken.raw(session.expiresAt ?? 'unknown')}`);
63
- if (isSessionExpired(session)) {
64
- session = await refreshStoreToken(session);
65
- }
66
- return session;
67
- }
68
- async function resolveApiVersion(options) {
69
- const { session, adminSession, userSpecifiedVersion } = options;
70
- if (userSpecifiedVersion === 'unstable')
71
- return userSpecifiedVersion;
72
- let availableVersions;
73
- try {
74
- availableVersions = await fetchApiVersions(adminSession);
75
- }
76
- catch (error) {
77
- if (error instanceof AbortError &&
78
- error.message.includes(`Error connecting to your store ${adminSession.storeFqdn}:`) &&
79
- /\b(?:401|404)\b/.test(error.message)) {
80
- clearStoredStoreAppSession(session.store, session.userId);
81
- throw reauthenticateStoreAuthError(`Stored app authentication for ${session.store} is no longer valid.`, session.store, session.scopes.join(','));
82
- }
83
- throw error;
84
- }
85
- if (!userSpecifiedVersion) {
86
- const supportedVersions = availableVersions.filter((version) => version.supported).map((version) => version.handle);
87
- return supportedVersions.sort().reverse()[0];
88
- }
89
- const versionList = availableVersions.map((version) => version.handle);
90
- if (versionList.includes(userSpecifiedVersion))
91
- return userSpecifiedVersion;
92
- throw new AbortError(`Invalid API version: ${userSpecifiedVersion}`, `Allowed versions: ${versionList.join(', ')}`);
93
- }
94
- export async function prepareAdminStoreGraphQLContext(input) {
95
- const session = await loadStoredStoreSession(input.store);
96
- const adminSession = {
97
- token: session.accessToken,
98
- storeFqdn: session.store,
99
- };
100
- const version = await resolveApiVersion({ session, adminSession, userSpecifiedVersion: input.userSpecifiedVersion });
101
- return { adminSession, version, sessionUserId: session.userId };
102
- }
103
- //# sourceMappingURL=admin-graphql-context.js.map
@@ -1,9 +0,0 @@
1
- import { AdminSession } from '@shopify/cli-kit/node/session';
2
- import { PreparedStoreExecuteRequest } from './execute-request.js';
3
- export declare function runAdminStoreGraphQLOperation(input: {
4
- store: string;
5
- adminSession: AdminSession;
6
- sessionUserId: string;
7
- version: string;
8
- request: PreparedStoreExecuteRequest;
9
- }): Promise<unknown>;
@@ -1,61 +0,0 @@
1
- import { openURL } from '@shopify/cli-kit/node/system';
2
- interface StoreAuthInput {
3
- store: string;
4
- scopes: string;
5
- }
6
- interface StoreTokenResponse {
7
- access_token: string;
8
- token_type?: string;
9
- scope?: string;
10
- expires_in?: number;
11
- refresh_token?: string;
12
- refresh_token_expires_in?: number;
13
- associated_user_scope?: string;
14
- associated_user?: {
15
- id: number;
16
- first_name?: string;
17
- last_name?: string;
18
- email?: string;
19
- account_owner?: boolean;
20
- locale?: string;
21
- collaborator?: boolean;
22
- email_verified?: boolean;
23
- };
24
- }
25
- interface WaitForAuthCodeOptions {
26
- store: string;
27
- state: string;
28
- port: number;
29
- timeoutMs?: number;
30
- onListening?: () => void | Promise<void>;
31
- }
32
- export declare function generateCodeVerifier(): string;
33
- export declare function computeCodeChallenge(verifier: string): string;
34
- export declare function parseStoreAuthScopes(input: string): string[];
35
- export declare function buildStoreAuthUrl(options: {
36
- store: string;
37
- scopes: string[];
38
- state: string;
39
- redirectUri: string;
40
- codeChallenge: string;
41
- }): string;
42
- export declare function waitForStoreAuthCode({ store, state, port, timeoutMs, onListening, }: WaitForAuthCodeOptions): Promise<string>;
43
- export declare function exchangeStoreAuthCodeForToken(options: {
44
- store: string;
45
- code: string;
46
- codeVerifier: string;
47
- redirectUri: string;
48
- }): Promise<StoreTokenResponse>;
49
- interface StoreAuthPresenter {
50
- openingBrowser: () => void;
51
- manualAuthUrl: (authorizationUrl: string) => void;
52
- success: (store: string, email?: string) => void;
53
- }
54
- interface StoreAuthDependencies {
55
- openURL: typeof openURL;
56
- waitForStoreAuthCode: typeof waitForStoreAuthCode;
57
- exchangeStoreAuthCodeForToken: typeof exchangeStoreAuthCodeForToken;
58
- presenter: StoreAuthPresenter;
59
- }
60
- export declare function authenticateStoreWithApp(input: StoreAuthInput, dependencies?: StoreAuthDependencies): Promise<void>;
61
- export {};
@@ -1,326 +0,0 @@
1
- import { DEFAULT_STORE_AUTH_PORT, STORE_AUTH_APP_CLIENT_ID, STORE_AUTH_CALLBACK_PATH, maskToken, storeAuthRedirectUri } from './auth-config.js';
2
- import { retryStoreAuthWithPermanentDomainError } from './auth-recovery.js';
3
- import { setStoredStoreAppSession } from './session.js';
4
- import { normalizeStoreFqdn } from '@shopify/cli-kit/node/context/fqdn';
5
- import { randomUUID } from '@shopify/cli-kit/node/crypto';
6
- import { AbortError } from '@shopify/cli-kit/node/error';
7
- import { fetch } from '@shopify/cli-kit/node/http';
8
- import { outputCompleted, outputContent, outputDebug, outputInfo, outputToken } from '@shopify/cli-kit/node/output';
9
- import { openURL } from '@shopify/cli-kit/node/system';
10
- import { createHash, randomBytes, timingSafeEqual } from 'crypto';
11
- import { createServer } from 'http';
12
- export function generateCodeVerifier() {
13
- return randomBytes(32).toString('base64url');
14
- }
15
- export function computeCodeChallenge(verifier) {
16
- return createHash('sha256').update(verifier).digest('base64url');
17
- }
18
- export function parseStoreAuthScopes(input) {
19
- const scopes = input
20
- .split(',')
21
- .map((scope) => scope.trim())
22
- .filter(Boolean);
23
- if (scopes.length === 0) {
24
- throw new AbortError('At least one scope is required.', 'Pass --scopes as a comma-separated list.');
25
- }
26
- return [...new Set(scopes)];
27
- }
28
- function expandImpliedStoreScopes(scopes) {
29
- const expandedScopes = new Set(scopes);
30
- for (const scope of scopes) {
31
- const matches = scope.match(/^(unauthenticated_)?write_(.*)$/);
32
- if (matches) {
33
- expandedScopes.add(`${matches[1] ?? ''}read_${matches[2]}`);
34
- }
35
- }
36
- return expandedScopes;
37
- }
38
- function resolveGrantedScopes(tokenResponse, requestedScopes) {
39
- if (!tokenResponse.scope) {
40
- outputDebug(outputContent `Token response did not include scope; falling back to requested scopes`);
41
- return requestedScopes;
42
- }
43
- const grantedScopes = parseStoreAuthScopes(tokenResponse.scope);
44
- const expandedGrantedScopes = expandImpliedStoreScopes(grantedScopes);
45
- const missingScopes = requestedScopes.filter((scope) => !expandedGrantedScopes.has(scope));
46
- if (missingScopes.length > 0) {
47
- throw new AbortError('Shopify granted fewer scopes than were requested.', `Missing scopes: ${missingScopes.join(', ')}.`, [
48
- 'Update the app or store installation scopes.',
49
- 'See https://shopify.dev/app/scopes',
50
- 'Re-run shopify store auth.',
51
- ]);
52
- }
53
- return grantedScopes;
54
- }
55
- export function buildStoreAuthUrl(options) {
56
- const params = new URLSearchParams();
57
- params.set('client_id', STORE_AUTH_APP_CLIENT_ID);
58
- params.set('scope', options.scopes.join(','));
59
- params.set('redirect_uri', options.redirectUri);
60
- params.set('state', options.state);
61
- params.set('response_type', 'code');
62
- params.set('code_challenge', options.codeChallenge);
63
- params.set('code_challenge_method', 'S256');
64
- return `https://${options.store}/admin/oauth/authorize?${params.toString()}`;
65
- }
66
- function renderAuthCallbackPage(title, message) {
67
- const safeTitle = title
68
- .replace(/&/g, '&amp;')
69
- .replace(/</g, '&lt;')
70
- .replace(/>/g, '&gt;')
71
- .replace(/"/g, '&quot;');
72
- const safeMessage = message
73
- .replace(/&/g, '&amp;')
74
- .replace(/</g, '&lt;')
75
- .replace(/>/g, '&gt;')
76
- .replace(/"/g, '&quot;');
77
- return `<!doctype html>
78
- <html lang="en">
79
- <head>
80
- <meta charset="utf-8" />
81
- <meta name="viewport" content="width=device-width, initial-scale=1" />
82
- <title>${safeTitle}</title>
83
- </head>
84
- <body style="margin:0;background:#f6f6f7;color:#202223;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
85
- <main style="max-width:32rem;margin:12vh auto;padding:0 1rem;">
86
- <section style="background:#fff;border:1px solid #e1e3e5;border-radius:12px;padding:1.5rem 1.25rem;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
87
- <h1 style="margin:0 0 0.75rem 0;font-size:1.375rem;line-height:1.2;">${safeTitle}</h1>
88
- <p style="margin:0;font-size:1rem;line-height:1.5;">${safeMessage}</p>
89
- </section>
90
- </main>
91
- </body>
92
- </html>`;
93
- }
94
- export async function waitForStoreAuthCode({ store, state, port, timeoutMs = 5 * 60 * 1000, onListening, }) {
95
- const normalizedStore = normalizeStoreFqdn(store);
96
- return new Promise((resolve, reject) => {
97
- let settled = false;
98
- let isListening = false;
99
- const timeout = setTimeout(() => {
100
- settleWithError(new AbortError('Timed out waiting for OAuth callback.'));
101
- }, timeoutMs);
102
- const server = createServer((req, res) => {
103
- const requestUrl = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
104
- if (requestUrl.pathname !== STORE_AUTH_CALLBACK_PATH) {
105
- res.statusCode = 404;
106
- res.end('Not found');
107
- return;
108
- }
109
- const { searchParams } = requestUrl;
110
- const fail = (error, tryMessage) => {
111
- const abortError = typeof error === 'string' ? new AbortError(error, tryMessage) : error;
112
- res.statusCode = 400;
113
- res.setHeader('Content-Type', 'text/html');
114
- res.setHeader('Connection', 'close');
115
- res.once('finish', () => settleWithError(abortError));
116
- res.end(renderAuthCallbackPage('Authentication failed', abortError.message));
117
- };
118
- const returnedStore = searchParams.get('shop');
119
- outputDebug(outputContent `Received OAuth callback for shop ${outputToken.raw(returnedStore ?? 'unknown')}`);
120
- if (!returnedStore) {
121
- fail('OAuth callback store does not match the requested store.');
122
- return;
123
- }
124
- const normalizedReturnedStore = normalizeStoreFqdn(returnedStore);
125
- if (normalizedReturnedStore !== normalizedStore) {
126
- fail(retryStoreAuthWithPermanentDomainError(normalizedReturnedStore));
127
- return;
128
- }
129
- const returnedState = searchParams.get('state');
130
- if (!returnedState || !constantTimeEqual(returnedState, state)) {
131
- fail('OAuth callback state does not match the original request.');
132
- return;
133
- }
134
- const error = searchParams.get('error');
135
- if (error) {
136
- fail(`Shopify returned an OAuth error: ${error}`);
137
- return;
138
- }
139
- const code = searchParams.get('code');
140
- if (!code) {
141
- fail('OAuth callback did not include an authorization code.');
142
- return;
143
- }
144
- outputDebug(outputContent `Received authorization code ${outputToken.raw(maskToken(code))}`);
145
- res.statusCode = 200;
146
- res.setHeader('Content-Type', 'text/html');
147
- res.setHeader('Connection', 'close');
148
- res.once('finish', () => settle(() => resolve(code)));
149
- res.end(renderAuthCallbackPage('Authentication succeeded', 'You can close this window and return to the terminal.'));
150
- });
151
- const settle = (callback) => {
152
- if (settled)
153
- return;
154
- settled = true;
155
- clearTimeout(timeout);
156
- const finalize = () => {
157
- callback();
158
- };
159
- if (!isListening) {
160
- finalize();
161
- return;
162
- }
163
- server.close(() => {
164
- isListening = false;
165
- finalize();
166
- });
167
- server.closeIdleConnections?.();
168
- };
169
- const settleWithError = (error) => {
170
- settle(() => reject(error));
171
- };
172
- server.on('error', (error) => {
173
- if (error.code === 'EADDRINUSE') {
174
- settleWithError(new AbortError(`Port ${port} is already in use.`, `Free port ${port} and re-run ${outputToken.genericShellCommand(`shopify store auth --store ${store} --scopes <comma-separated-scopes>`).value}. Ensure that redirect URI is allowed in the app configuration.`));
175
- return;
176
- }
177
- settleWithError(error);
178
- });
179
- server.listen(port, '127.0.0.1', async () => {
180
- isListening = true;
181
- outputDebug(outputContent `PKCE callback server listening on http://127.0.0.1:${outputToken.raw(String(port))}${outputToken.raw(STORE_AUTH_CALLBACK_PATH)}`);
182
- if (!onListening)
183
- return;
184
- try {
185
- await onListening();
186
- }
187
- catch (error) {
188
- settleWithError(error instanceof Error ? error : new Error(String(error)));
189
- }
190
- });
191
- });
192
- }
193
- function constantTimeEqual(a, b) {
194
- if (a.length !== b.length)
195
- return false;
196
- return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));
197
- }
198
- export async function exchangeStoreAuthCodeForToken(options) {
199
- const endpoint = `https://${options.store}/admin/oauth/access_token`;
200
- outputDebug(outputContent `Exchanging authorization code for token at ${outputToken.raw(endpoint)}`);
201
- const response = await fetch(endpoint, {
202
- method: 'POST',
203
- headers: { 'Content-Type': 'application/json' },
204
- body: JSON.stringify({
205
- client_id: STORE_AUTH_APP_CLIENT_ID,
206
- code: options.code,
207
- code_verifier: options.codeVerifier,
208
- redirect_uri: options.redirectUri,
209
- }),
210
- });
211
- const body = await response.text();
212
- if (!response.ok) {
213
- outputDebug(outputContent `Token exchange failed with HTTP ${outputToken.raw(String(response.status))}: ${outputToken.raw(body.slice(0, 300))}`);
214
- throw new AbortError(`Failed to exchange OAuth code for an access token (HTTP ${response.status}).`, body || response.statusText);
215
- }
216
- let parsed;
217
- try {
218
- parsed = JSON.parse(body);
219
- }
220
- catch {
221
- throw new AbortError('Received an invalid token response from Shopify.');
222
- }
223
- 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')})`);
224
- return parsed;
225
- }
226
- const defaultStoreAuthPresenter = {
227
- openingBrowser() {
228
- outputInfo('Shopify CLI will open the app authorization page in your browser.');
229
- outputInfo('');
230
- },
231
- manualAuthUrl(authorizationUrl) {
232
- outputInfo('Browser did not open automatically. Open this URL manually:');
233
- outputInfo(outputContent `${outputToken.link(authorizationUrl)}`);
234
- outputInfo('');
235
- },
236
- success(store, email) {
237
- const displayName = email ? ` as ${email}` : '';
238
- outputCompleted('Logged in.');
239
- outputCompleted(`Authenticated${displayName} against ${store}.`);
240
- outputInfo('');
241
- outputInfo('To verify that authentication worked, run:');
242
- outputInfo(`shopify store execute --store ${store} --query 'query { shop { name id } }'`);
243
- },
244
- };
245
- const defaultStoreAuthDependencies = {
246
- openURL,
247
- waitForStoreAuthCode,
248
- exchangeStoreAuthCodeForToken,
249
- presenter: defaultStoreAuthPresenter,
250
- };
251
- function createPkceBootstrap(input, exchangeCodeForToken) {
252
- const store = normalizeStoreFqdn(input.store);
253
- const scopes = parseStoreAuthScopes(input.scopes);
254
- const port = DEFAULT_STORE_AUTH_PORT;
255
- const state = randomUUID();
256
- const redirectUri = storeAuthRedirectUri(port);
257
- const codeVerifier = generateCodeVerifier();
258
- const codeChallenge = computeCodeChallenge(codeVerifier);
259
- const authorizationUrl = buildStoreAuthUrl({ store, scopes, state, redirectUri, codeChallenge });
260
- outputDebug(outputContent `Starting PKCE auth for ${outputToken.raw(store)} with scopes ${outputToken.raw(scopes.join(','))} (redirect_uri=${outputToken.raw(redirectUri)})`);
261
- return {
262
- authorization: {
263
- store,
264
- scopes,
265
- state,
266
- port,
267
- redirectUri,
268
- authorizationUrl,
269
- codeVerifier,
270
- },
271
- waitForAuthCodeOptions: {
272
- store,
273
- state,
274
- port,
275
- },
276
- exchangeCodeForToken: (code) => exchangeCodeForToken({ store, code, codeVerifier, redirectUri }),
277
- };
278
- }
279
- export async function authenticateStoreWithApp(input, dependencies = defaultStoreAuthDependencies) {
280
- const bootstrap = createPkceBootstrap(input, dependencies.exchangeStoreAuthCodeForToken);
281
- const { authorization: { store, scopes, redirectUri, authorizationUrl }, } = bootstrap;
282
- dependencies.presenter.openingBrowser();
283
- const code = await dependencies.waitForStoreAuthCode({
284
- ...bootstrap.waitForAuthCodeOptions,
285
- onListening: async () => {
286
- const opened = await dependencies.openURL(authorizationUrl);
287
- if (!opened)
288
- dependencies.presenter.manualAuthUrl(authorizationUrl);
289
- },
290
- });
291
- const tokenResponse = await bootstrap.exchangeCodeForToken(code);
292
- const userId = tokenResponse.associated_user?.id?.toString();
293
- if (!userId) {
294
- throw new AbortError('Shopify did not return associated user information for the online access token.');
295
- }
296
- const now = Date.now();
297
- const expiresAt = tokenResponse.expires_in ? new Date(now + tokenResponse.expires_in * 1000).toISOString() : undefined;
298
- setStoredStoreAppSession({
299
- store,
300
- clientId: STORE_AUTH_APP_CLIENT_ID,
301
- userId,
302
- accessToken: tokenResponse.access_token,
303
- refreshToken: tokenResponse.refresh_token,
304
- // Store the raw scopes returned by Shopify. Validation may treat implied
305
- // write_* -> read_* permissions as satisfied, so callers should not assume
306
- // session.scopes is an expanded/effective permission set.
307
- scopes: resolveGrantedScopes(tokenResponse, scopes),
308
- acquiredAt: new Date(now).toISOString(),
309
- expiresAt,
310
- refreshTokenExpiresAt: tokenResponse.refresh_token_expires_in
311
- ? new Date(now + tokenResponse.refresh_token_expires_in * 1000).toISOString()
312
- : undefined,
313
- associatedUser: tokenResponse.associated_user
314
- ? {
315
- id: tokenResponse.associated_user.id,
316
- email: tokenResponse.associated_user.email,
317
- firstName: tokenResponse.associated_user.first_name,
318
- lastName: tokenResponse.associated_user.last_name,
319
- accountOwner: tokenResponse.associated_user.account_owner,
320
- }
321
- : undefined,
322
- });
323
- outputDebug(outputContent `Session persisted for ${outputToken.raw(store)} (user ${outputToken.raw(userId)}, expires ${outputToken.raw(expiresAt ?? 'unknown')})`);
324
- dependencies.presenter.success(store, tokenResponse.associated_user?.email);
325
- }
326
- //# sourceMappingURL=auth.js.map
@@ -1 +0,0 @@
1
- export declare function writeOrOutputStoreExecuteResult(result: unknown, outputFile?: string): Promise<void>;
@@ -1,18 +0,0 @@
1
- import { writeFile } from '@shopify/cli-kit/node/fs';
2
- import { outputResult } from '@shopify/cli-kit/node/output';
3
- import { renderSuccess } from '@shopify/cli-kit/node/ui';
4
- export async function writeOrOutputStoreExecuteResult(result, outputFile) {
5
- const resultString = JSON.stringify(result, null, 2);
6
- if (outputFile) {
7
- await writeFile(outputFile, resultString);
8
- renderSuccess({
9
- headline: 'Operation succeeded.',
10
- body: `Results written to ${outputFile}`,
11
- });
12
- }
13
- else {
14
- renderSuccess({ headline: 'Operation succeeded.' });
15
- outputResult(resultString);
16
- }
17
- }
18
- //# sourceMappingURL=execute-result.js.map