Add production preview store create command#7764
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
a95b7a4 to
897e9ea
Compare
e6b9771 to
f0160e7
Compare
72f8f19 to
b504c98
Compare
b504c98 to
c3f7626
Compare
| const acquiredAt = dependencies.now().toISOString() | ||
| const userId = previewUserId(response) | ||
|
|
||
| await dependencies.recordStoreFqdnMetadata(response.shop.domain, false) |
There was a problem hiding this comment.
🐛 Bug: Two concerns at this call site, both pointing at the same fix:
-
Ordering risk: Both
recordStoreFqdnMetadatacalls precedesetStoredStoreAppSession. The testdoes not persist a store session when recording store metadata failslocks this in, but at that point the backend has already created the preview store and issued an admin token — if metadata throws (e.g., 'bubble' contexts), the token is dropped and the merchant is left with an orphaned store and no local credential. Metadata is best-effort observability; it should not gate token persistence. -
Redundant pair of calls: In
auth/index.tsthevalidated:false→validated:truetransition straddles the OAuth handshake, so both states are meaningful. Here the two calls fire back-to-back with no intervening validation step, andrecordStoreFqdnMetadataObject.assigns into a shared bag — so thefalsewrite is immediately overwritten bytrueand adds no telemetry signal.
Suggestion: Persist the session first, then record metadata once with true:
| await dependencies.recordStoreFqdnMetadata(response.shop.domain, false) | |
| dependencies.setStoredStoreAppSession({ | |
| store: response.shop.domain, | |
| clientId: STORE_AUTH_APP_CLIENT_ID, | |
| userId, | |
| accessToken: response.adminApiToken, | |
| scopes: [], | |
| acquiredAt, | |
| kind: 'preview', | |
| preview: { | |
| shopId: response.shop.id, | |
| name: response.shop.name, | |
| createdAt: acquiredAt, | |
| ...(response.placeholderAccountUuid ? {placeholderAccountUuid: response.placeholderAccountUuid} : {}), | |
| ...(country ? {country} : {}), | |
| }, | |
| }) | |
| dependencies.setLastSeenUserId(userId) | |
| await dependencies.recordStoreFqdnMetadata(response.shop.domain, true) |
c3f7626 to
07d1745
Compare
07d1745 to
f4ec45d
Compare
| let _clientStorage: LocalStorage<PreviewStoreClientStorageSchema> | undefined | ||
|
|
||
| function clientStorage() { | ||
| _clientStorage ??= new LocalStorage<PreviewStoreClientStorageSchema>({projectName: 'shopify-cli-store'}) |
There was a problem hiding this comment.
Nit: Should we extract this into a shared constant?
| accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', | ||
| requestedCountry: 'US', | ||
| }, | ||
| nextSteps: [ |
There was a problem hiding this comment.
These next steps might drift from the non-JSON result. Any way to consolidate?
| const message = error instanceof Error ? error.message : 'Unknown error' | ||
| throw new AbortError( | ||
| 'Preview store creation returned a non-JSON response.', | ||
| `Parse error: ${message}. Body (truncated): ${rawText.slice(0, 500)}`, |
There was a problem hiding this comment.
🔒 Security: Consider reviewing the JSON parse failure path because it includes the raw response body in the user-visible AbortError diagnostic. The preview-store endpoint can return an Admin API token and tokenized access URL, and the current redaction helper only runs after JSON parsing succeeds. A malformed or truncated credential-bearing response could therefore expose secrets in CLI output or copied bug reports.
Suggestion: Redact raw response text before adding it to any AbortError try message, or avoid including the body for this endpoint. The redaction should cover at least admin_api_token, access_url, and token-shaped preview-store access URLs. It would also be worth applying the same protection to any other fallback diagnostics that include raw response text for this endpoint.
| }, | ||
| }) | ||
| dependencies.setLastSeenUserId(userId) | ||
| await dependencies.recordStoreFqdnMetadata(response.shop.domain, true) |
There was a problem hiding this comment.
🐛 Bug: Worth reviewing this await because it can turn a successful remote store creation into a failed command after credentials have already been persisted locally. If recordStoreFqdnMetadata rejects, the user may see a failure and never receive the access URL, even though the preview store exists and the Admin API token is cached. That conflicts with the intended best-effort nature of this metadata write and can encourage duplicate retries.
Suggestion: Treat recordStoreFqdnMetadata as non-blocking for command success. Catch failures after persisting the session and last-seen user, optionally debug-log them, and still return the success result so the user receives the access URL.
| await dependencies.recordStoreFqdnMetadata(response.shop.domain, true) | |
| try { | |
| await dependencies.recordStoreFqdnMetadata(response.shop.domain, true) | |
| } catch { | |
| // Store metadata is best-effort; credentials and access URL are already persisted. | |
| } |

WHY are these changes introduced?
This productionizes
shopify store create previewon top of the shipped preview-store backend endpoint so an agent can create a preview store and immediately use the returned Admin API token through existing store command auth plumbing.The command targets the production endpoint contract from shop/world:
POST /services/preview-storesnamevariables.storeCreatePayload.countryshopdetails,placeholder_account_uuid,admin_api_token, andaccess_urlWHAT is this pull request doing?
shopify store create previewwith flags:--name--countrywith two-letter code validation--json--no-color/--verbosehttps://<app-management-fqdn>/services/preview-storeswithout Basic auth or Identity auth.X-Shopify-CLI-Instancefrom a stable locally persisted install idX-Shopify-CLI-VersionUser-Agentvariables.storeCreatePayload.countrywhen--countryis providedshop.idshop.nameshop.domainplaceholder_account_uuidwhen presentadmin_api_tokenaccess_urlkind: 'preview', so the store is immediately usable byshopify store execute --store <domain>withoutshopify store auth.admin_api_tokenand tokenizedaccess_urlfrom malformed-response diagnostics.Notes / open questions
422 preview_store_create_failedpath and the currently observed backend500 preview_store_create_failedpath defensively.How to test your changes?
Ideally, the endpoint will be ready in prod to test this (protected behind a flag) and we can test as follows:
pnpm shopify store create preview --country USpnpm shopify store execute --store <returned-domain>should use the cached preview-store Admin API tokenLocal validation run:
pnpm --filter @shopify/store exec vitest run src/cli/commands/store/create/preview.test.ts src/cli/services/store/create/preview/client.test.ts src/cli/services/store/create/preview/index.test.ts src/cli/services/store/create/preview/result.test.ts src/cli/services/store/auth/session-store.test.tspnpm nx run store:lint --skip-nx-cache --output-style=streampnpm --filter @shopify/store run type-check/usr/bin/git diff --checkPost-release steps
None.
Checklist
LocalStorage; command logic is platform-neutral.@shopify/cliand@shopify/store.