Skip to content

Handle Trust Battery challenges in theme commands - #8562

Merged
iaineh merged 7 commits into
mainfrom
gg-theme-trust-battery-cli
Oct 20, 2026
Merged

iaineh merged 7 commits into
mainfrom
gg-theme-trust-battery-cli

Conversation

@graygilmore

@graygilmore graygilmore commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #8897 (merged). That PR routes the theme mutations through requestThemeAdminDoc, so this PR only wraps that one function.

WHY are these changes introduced?

Core runs a Trust Battery session decision on every theme mutation (themeFilesUpsert, themeFilesDelete, themePublish, themeCreate, ...). It also runs it for theme file reads that pass a sessionId, but the CLI's read queries send no sessionId. When the session looks risky the decision returns an identity challenge. Admin Web can show that challenge; the CLI can't, so Core currently suppresses challenges for CLI callers. That leaves the CLI as a way around the check.

Core is going to start surfacing challenges to the CLI behind a shop flag (shop/world#2068823), gated on the CLI version from the User-Agent, so this can be dogfooded before release and old CLIs do not receive unsupported challenges. This PR is the CLI half.

WHAT is this pull request doing?

Theme Admin API requests now recognise a CHALLENGE_REQUIRED GraphQL error and:

  1. open the redirect_to challenge URL in the browser (or show the link if the browser can't be opened),
  2. show a banner and wait for Enter,
  3. retry the request once.

What it looks like mid-theme push:

Uploading files to remote theme [0%] ...
╭─ info ─────────────────────────────────────────────────────────╮
│                                                                │
│  Verify your identity to continue.                             │
│                                                                │
│  A verification page opened in your browser.                   │
│                                                                │
│  Next steps                                                    │
│    1. Complete the verification in your browser, logged in     │
│       with the same account you use in Shopify CLI.            │
│    2. Press Enter here once you have completed it.             │
│                                                                │
╰────────────────────────────────────────────────────────────────╯

then after Enter:

╭─ success ──────────────────────────────────────────────────────╮
│                                                                │
│  Continuing.                                                   │
│                                                                │
╰────────────────────────────────────────────────────────────────╯
Uploading files to remote theme [100%] ...

A second challenge on the retry aborts with the URL instead of looping. In CI, without a TTY, with --no-input, or with --json, the CLI aborts straight away with the URL and next steps.

Some details worth knowing:

  • Only mutations are challenged today. Core runs the decision for files/file reads only when the query passes sessionId (breadth_resolvers/online_store_theme/files.rb). getThemeFileChecksums and getThemeFileBodies send none, so fetchChecksums, the theme dev poll, and theme pull are not challenged. Reads still go through requestThemeAdminDoc (theme mutations moved there in Route theme mutations through requestThemeAdminDoc #8897), so a read challenge would get the same handling. That case is not tested end to end.
  • The browser must use the same account. Identity rejects a verification made with a different account (mismatched_identity_account), so the banner and the abort message both say to log in with the same account you use in Shopify CLI.
  • Non-interactive modes abort instead of hanging. --json (and SHOPIFY_FLAG_JSON=1) turns the banner into a single stderr JSON diagnostic, and the Enter prompt would block with no visible text, so JSON output mode now aborts with the URL like CI and non-TTY already did.
  • theme dev exits cleanly on Ctrl+C at the prompt. The prompt's Ctrl+C throws AbortSilentError, which the dev poll loop and upload catches were rendering as an empty "Error while polling for changes." / "Failed to upload file" banner. renderThrownError now returns early for AbortSilentError; theme dev's own Ctrl+C handler does the exit.
  • Scoped to theme requests. The handling lives in requestThemeAdminDoc in cli-kit/.../themes/api.ts. app dev with a theme app extension also reaches it: HostThemeManager.createHostTheme calls themeCreate, and Core runs SessionTrustedToCreateTheme on it. That retry loop caught every error, so Ctrl+C at the prompt or a failed verification started a new attempt, up to four prompts. The challenge aborts are now a TrustChallengeError, the themes API exports isTrustChallengeError, and the loop rethrows it and AbortSilentError. Other theme creation errors are still retried. Not verified: how the banner renders under app dev's renderTasks spinner.
  • One prompt for concurrent requests. theme push fires upload batches in parallel and they all get challenged together. A shared in-flight promise means the browser opens once, every waiting batch retries after the same Enter, and the success banner renders once. A request that starts while a challenge is pending (e.g. saving another file in theme dev) waits for it first, so it's sent once instead of being challenged and retried too. A batch that was already in flight when the user pressed Enter, and gets its challenge only after that, retries once without a second prompt: each request notes how many challenges were completed when it started.
  • Enter/Space only. theme dev keeps stdin in raw mode and binds t/p/e/g on the same stream. cli-kit's keypress() now accepts a keys option and sets raw mode back to its earlier state; the prompt calls it with Enter and Space, so other keys never answer it. Ctrl+C still aborts. (Before, keypress() always turned raw mode off when it finished, which would have broken theme dev's shortcuts.)
  • Everything is a banner, and keys are compared as text. Both because of Ink. When theme push's progress bar is running, Ink redraws its frame and erases plain lines written underneath it, so the "press Enter" instruction lives inside the info banner rather than a separate outputInfo. Ink's useInput also calls stdin.setEncoding('utf8'), so stdin emits strings instead of Buffers; keypress() now reads each chunk as text before it matches keys or Ctrl+C. This also fixes Ctrl+C detection for keypress()'s other caller (mkcert). I found both of these tophatting, the first version used outputInfo and byte comparisons and looked frozen under theme push.
  • Only Admin-host URLs are honoured. redirect_to must be https:// on adminFqdn(); anything else falls through as a normal error, same as Admin Web.
  • Theme Access / custom-app tokens never get a challenge (no identity session on Core's side), so CI pushes using shptka_/shpat_ are unaffected.

Analytics: theme-api:trust-challenge:{required,browser-opened,browser-not-opened,acknowledged,retry-challenged,non-interactive} via recordEvent.

How to manually test your changes?

Local end-to-end, done 2026-10-14. Setup: local Trust Battery, Identity, admin-web, storefront, and Core on shop/world#2068823. Flag on for shop 1. CLI from this branch with packages/cli-kit/package.json set to 4.10.0 (not committed; rebuild with pnpm nx build cli --skip-nx-cache after each version change). A Trust Battery decision override forces challenge (identity_session_user_verification) on SessionTrustedToUpsertThemeFiles for the CLI session.

  1. Push with no override: succeeds.
  2. Override active, push: the browser opens https://admin.shop.dev/challenges/user_verification?token=… and the CLI shows the info banner above. Core logs:
    theme_trust_battery_challenge_surfaced: Trust Battery challenge surfaced to a challenge-capable theme API caller
    Attributes: api_version=2026-10, shop_id=1, api_client_id=4988523, mutation_name=ThemeFilesUpsert, cli_version=4.10.0
    
  3. Verification in the browser with the same account. The CLI session's battery then has identity_session_user_verified = success. Override off, Enter: the success banner, then "The theme … was pushed successfully."
  4. Override on again, Enter without verifying: "Shopify still needs to verify your identity before it can continue." with the URL. This run showed that the old success text ("Verification complete.") rendered before the failed retry, so the banner now says "Continuing."
  5. Override on:
    • --json: exit 1, no browser, the abort with the URL as a JSON error document on stdout.
    • CI=1 and --no-input: exit 1, the abort banner with the URL.
    • CLI built as 4.9.3: push succeeds. Core logs theme_trust_battery_challenge_suppressed with cli_version=4.9.3.
    • 4.10.0 with the flag off: push succeeds, suppressed with cli_version=4.10.0.
  6. theme dev, override on: the initial-sync upload (themeFilesUpsert) is challenged and the prompt shows under the preview links after about 4 s. t does not answer the prompt (theme dev's own t shortcut still opens the preview). Ctrl+C at the prompt exits with code 0 and no "Error while polling" / "Failed to upload" / "Failed to perform the initial theme synchronization" banner. Not verified: a challenge on a poll or a read. The poll reads send no sessionId, so Core does not challenge them.

Out of scope, seen on the base branch too: in --json mode, an upload-phase error prints its JSON document twice, and theme dev renders the initial-sync error banner twice.

Without local Core you can fake the response:

  1. In packages/cli-kit/src/public/node/api/admin.ts, temporarily make adminRequestDoc reject once with:
    throw new ClientError(
      {status: 200, errors: [{message: 'Challenge Required', extensions: {code: 'CHALLENGE_REQUIRED', redirect_to: 'https://admin.shopify.com/challenges/user_verification?token=test'}}] as any},
      {query: ''},
    )
  2. pnpm shopify theme push --store <store> --development (Identity login, not --password).
  3. Expect: a browser tab opens on admin.shopify.com/challenges/... and the info banner above appears. Press t — nothing happens. Press Enter — the success banner appears and the push completes.
  4. Run with CI=1 (or --no-input, or --json) and expect an immediate abort listing the challenge URL as a next step.

Post-release steps

Core PR shop/world#2068823 pins MIN_CHALLENGE_CAPABLE_CLI_VERSION to the first release that contains this PR's merge commit (expected 4.10.0). If this ships under a different version, that constant needs to change before the Core PR merges.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset add

@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Sep 16, 2026
@graygilmore
graygilmore force-pushed the gg-theme-trust-battery-cli branch 4 times, most recently from 7d6db8a to 945684a Compare September 24, 2026 00:45
@iaineh
iaineh force-pushed the gg-theme-trust-battery-cli branch from 945684a to de3151f Compare October 14, 2026 19:23
@iaineh iaineh changed the title Handle Trust Battery challenges for theme mutations Handle Trust Battery challenges in theme commands Oct 14, 2026
@iaineh
iaineh changed the base branch from main to iain/theme-api-route-mutations October 14, 2026 19:23

@iaineh iaineh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notes on the larger decisions in this PR, for reviewers.

): Promise<TResult> {
try {
const response = await adminRequestDoc(options)
const response = await withTrustChallengeRetry(() => adminRequestDoc(options))

@iaineh iaineh Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only api.ts change in this PR. #8897 moves the six theme mutations to requestThemeAdminDoc, so theme reads and mutations all go through this one wrapper.

Today Core challenges only the mutations. It runs the decision for files/file reads only when the query passes sessionId (breadth_resolvers/online_store_theme/files.rb), and getThemeFileChecksums / getThemeFileBodies send none. So fetchChecksums, the theme dev poll, and theme pull are not challenged. If Core starts to challenge them, they get the same handling here. That read case is not tested end to end.

Edited: an earlier version of this comment said that Core challenges reads. That was wrong.

Comment on lines +126 to +135
if (isCI() || !isTTY() || commandEventOutputMode() === 'json') {
recordEvent('theme-api:trust-challenge:non-interactive')
throw new AbortError('Shopify needs to verify your identity before it can continue.', undefined, [
[
'Open',
{link: {url: challengeUrl}},
"in a browser where you're logged in to Shopify with the same account you use in Shopify CLI, complete the verification, then run the command again.",
],
])
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON output mode aborts, the same as CI and no TTY. With --json, -j, or SHOPIFY_FLAG_JSON=1, renderInfo becomes one stderr JSON line and waitForEnter blocks with no visible prompt. The command looks hung. A script can only use an abort with the URL.

--no-input needs no extra check: isTTY() is false then (terminalSupportsPrompting).

Test: "aborts with the challenge URL instead of prompting in JSON output mode".

Comment on lines +142 to +158
renderInfo({
headline: 'Verify your identity to continue.',
body: opened
? 'A verification page opened in your browser.'
: 'Shopify needs to verify your identity before it can continue.',
link: opened ? undefined : {label: 'Open the verification page', url: challengeUrl},
nextSteps: [
['Complete the verification in your browser, logged in with the same account you use in Shopify CLI.'],
['Press', {userInput: 'Enter'}, 'here once you have completed it.'],
],
orderedNextSteps: true,
})

await waitForEnter(options.stdin ?? process.stdin)

recordEvent('theme-api:trust-challenge:acknowledged')
renderSuccess({headline: 'Verification complete. Continuing.'})

@iaineh iaineh Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy changes:

  1. "apply this theme change" is now "continue". The text no longer names the operation, so it stays correct for any theme request. Today only mutations get the challenge.
  2. The text says to use the same account in the browser as in the CLI. Identity stops with mismatched_identity_account if the accounts differ.

Edited: an earlier version said the reason was that Core also challenges reads. That was wrong: the CLI reads send no sessionId, so Core does not challenge them.

* @param error - The error to render.
*/
export function renderThrownError(headline: string, error: Error | AbortError) {
if (error instanceof AbortSilentError) return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ctrl+C at the prompt during theme dev rejects with AbortSilentError. That class extends FatalError, not AbortError, so this function rendered an empty "Error while polling for changes." or "Failed to upload file" banner. The early return covers the poll loop, the initial-sync abort, and the file-save catches. theme dev's own Ctrl+C handler (createKeypressHandler → onClose) does the exit.

Test: "renders nothing for AbortSilentError".

@iaineh
iaineh marked this pull request as ready for review October 15, 2026 12:22
@iaineh
iaineh requested review from a team as code owners October 15, 2026 12:22
@iaineh
iaineh force-pushed the gg-theme-trust-battery-cli branch 3 times, most recently from 8ab28d3 to 45a041a Compare October 15, 2026 13:10

const TRUST_CHALLENGE_ERROR_CODE = 'CHALLENGE_REQUIRED'

interface TrustChallengeOptions {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: TrustChallengeOptions.stdin is only used by one test, and that test mocks waitForEnter anyway. isTTY() also ignores it, so passing a custom stdin wouldn't change the TTY check the way throwInNonTTY({stdin}) does elsewhere. I'd drop the option and let waitForEnter default to process.stdin.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in db92f0f: I removed the stdin option. The prompt now always reads process.stdin.

stdin?: typeof process.stdin
}

let pendingChallenge: Promise<void> | undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a one-line comment on why pendingChallenge can be process-wide would help: only Identity sessions get challenged, and every multi-environment theme command requires --password, so a process that can be challenged only ever has one Identity session.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One shared prompt per process is safe. Core challenges only Identity sessions, and every multi-environment theme command needs --password. So a process that can get a challenge has only one Identity session.

const challengeUrl = await trustChallengeUrl(error)
if (!challengeUrl) throw error

await completeTrustChallenge(challengeUrl, options)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One ordering the concurrency test misses: two batches are in flight, the first gets challenged and the user presses Enter, and the second batch's challenge response only arrives after pendingChallenge is cleared. That opens a second tab and prompt for a request that was sent before the verification. Probably rare, but large upload batches make it more likely. One fix: keep a counter of completed challenges, note it when a request starts, and if a challenge comes back for a request that started before the last completed challenge, retry it once without prompting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in db92f0f. Each request notes how many challenges were completed when it started. If its challenge arrives after a verification that completed while it was in flight, it retries once without a second tab or prompt. 260c865 adds one more case: if another prompt is open at that moment, the request waits for that prompt first. Both orderings have tests in trust-challenge.test.ts.

const challengeUrl = await trustChallengeUrl(error)
if (!challengeUrl) throw error

recordEvent('theme-api:trust-challenge:retry-challenged')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking: if the retry is challenged again, we abort partway through a theme push. When the verification just hasn't caught up yet, that leaves a half-uploaded theme. Did you consider showing the prompt again (say up to 3 times) before aborting? Fine as is if a second challenge usually means something is actually wrong.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A second challenge right after Enter usually means the verification is not complete. One retry and then an abort with the URL keeps the behaviour clear. The abort message tells the user to run the command again.

): Promise<TResult> {
try {
const response = await adminRequestDoc(options)
const response = await withTrustChallengeRetry(() => adminRequestDoc(options))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app dev with a theme app extension goes through this too: HostThemeManager.createHostTheme calls themeCreate, and Core runs SessionTrustedToCreateTheme on it. Its retry loop catches everything, so a Ctrl+C at the prompt (AbortSilentError) or the "still needs to verify" abort gets swallowed and retried. That can mean up to four browser tabs and prompts before it gives up, all inside a renderTasks spinner. I think the loop should rethrow AbortError/AbortSilentError (or at least the challenge ones), and the description should drop "App commands are unchanged". Could you also check how the banner renders under the spinner?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in db92f0f. The challenge aborts are now a TrustChallengeError, and the themes API exports isTrustChallengeError. HostThemeManager's retry loop rethrows it and AbortSilentError, so Ctrl+C or a failed verification stops after one prompt; other theme creation errors are still retried. The PR description no longer says "App commands are unchanged". I have not checked how the banner renders under the renderTasks spinner; the description says so.

@iaineh
iaineh force-pushed the gg-theme-trust-battery-cli branch from 45a041a to 3a65a93 Compare October 16, 2026 12:22
@iaineh
iaineh force-pushed the iain/theme-api-route-mutations branch from 1a3a3cd to 309f40b Compare October 16, 2026 12:22
graygilmore and others added 6 commits October 19, 2026 21:33
Core runs a Trust Battery session decision on every theme
mutation. Admin Web can present the resulting identity challenge,
but the CLI cannot, so Core currently suppresses challenges for CLI
callers. That leaves the CLI as a way around the check, and Core will
start surfacing challenges to CLI versions that can handle them.

Theme Admin API requests now recognise a CHALLENGE_REQUIRED GraphQL
error, open the challenge URL in the browser, wait for the user
to press Enter once they have completed it, and retry the request
once. A second challenge on the retry aborts with the URL instead of
looping. Non-interactive runs (CI or no TTY) abort immediately with
the URL and next steps.

Concurrent requests, such as the parallel upload batches of `theme
push`, share one prompt so the browser opens once and every waiting
batch retries after the same verification. A request that starts while
a challenge is still pending waits for it first, so it is sent once
instead of being challenged and retried too.

The prompt accepts Enter or Space only. `theme dev` keeps stdin in
raw mode and binds single-letter shortcuts on the same stream; the
dedicated wait ignores other keys and leaves raw mode as it found it,
so answering the prompt never fires a shortcut.

Only challenge URLs on the Admin host are honoured; anything else
falls through to normal error handling. The handling is intentionally
scoped to the theme API layer; app commands are unchanged.
- Abort instead of prompting in JSON output mode
- Use copy that does not name the operation, and say that the browser must use the same account as the CLI
- Render nothing for AbortSilentError in theme dev polling and upload errors
- Cover @shopify/theme with the challenge changeset
The banner renders before the retry. The retry can still get a challenge. "Continuing." does not say that the verification is complete.
trustChallengeUrl now reads the errors with extractGraphQLErrors instead of its own copy.
- A request that was already in flight when a challenge was completed
  retries without a second prompt. A counter of completed challenges is
  noted when each request starts, so a late challenge response for an
  earlier request does not open another browser tab.
- Remove `TrustChallengeOptions.stdin`. Only one test used it, and
  `isTTY()` did not read it. `waitForEnter` uses its `process.stdin` default.
- The two challenge aborts are a `TrustChallengeError` (an `AbortError`),
  and the themes API exports `isTrustChallengeError`. The `app dev` host
  theme retry loop rethrows it and `AbortSilentError`, so Ctrl+C or a failed
  verification stops theme creation instead of prompting up to four times.
  Other theme creation errors are still retried.
A request that was in flight during an earlier verification retried at
once when its challenge arrived, even if a newer request had opened
another prompt in the meantime. That retry was challenged again and
aborted while the prompt was still waiting for Enter. Such a request now
waits for the open prompt first.
@iaineh
iaineh force-pushed the iain/theme-api-route-mutations branch from 055a626 to 18a57f0 Compare October 19, 2026 19:36
@iaineh
iaineh force-pushed the gg-theme-trust-battery-cli branch from 8c9b560 to 260c865 Compare October 19, 2026 19:36
Base automatically changed from iain/theme-api-route-mutations to main October 20, 2026 10:58
Comment on lines +49 to +53
try {
return await request()
} catch (error) {
const challengeUrl = await trustChallengeUrl(error)
if (!challengeUrl) throw error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same as lines 39-43, should it be extracted to a reusable function?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6f43f01. Both attempts now go through one helper, attemptRequest. It returns the result or the challenge URL, and throws any other error. withTrustChallengeRetry only decides what to do after each attempt: prompt once, then retry, then abort.

* @param stdin - The stream to read from; defaults to process.stdin.
* @returns A promise that resolves when Enter or Space is pressed.
*/
export function waitForEnter(stdin: typeof process.stdin = process.stdin): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we extend the existing keypress() helper to support accepted keys and preserve the previous raw mode? This also looks like a useful place to handle string chunks from Ink, so both callers use the same input handling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great shout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6f43f01. keypress() in public/node/ui.tsx now:

  • accepts a keys option: only those keys end the wait, and other keys are ignored. Without the option, any key ends it, as before.
  • sets raw mode back to its earlier state. Before, it always turned raw mode off, which would break theme dev's shortcuts.
  • reads each chunk as text, so it also detects Ctrl+C when Ink has set the stdin encoding. This fixes the same problem for mkcert, the other caller.

The Trust Battery prompt calls keypress(process.stdin, undefined, {keys: ['\r', '\n', ' ']}), and waitForEnter is removed. Its tests moved to the keypress tests in ui.test.ts, and a changeset covers the new option. I also checked it in a real terminal: with stdin already raw and UTF-8 encoded, t and p are ignored, Enter resolves, and raw mode stays on.

- withTrustChallengeRetry runs both attempts through one helper,
  attemptRequest. It returns the result or the challenge URL and throws
  any other error.
- keypress accepts a keys option. Only those keys end the wait. It sets
  raw mode back to its earlier state and reads chunks as text, so it
  also detects Ctrl+C when Ink has set the stdin encoding. The Trust
  Battery prompt calls keypress with Enter and Space, and the
  waitForEnter helper is removed.
@github-actions

Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/private/node/themes/trust-challenge.d.ts
import { AbortError } from '../../../public/node/error.js';
/**
 * The abort that ends a Trust Battery challenge: the session cannot answer it, or
 * the request was challenged again after the verification.
 */
export declare class TrustChallengeError extends AbortError {
}
/**
 * Runs a theme Admin API request and, when Core answers with a Trust Battery
 * challenge, waits for the user to complete it in the browser before retrying once.
 *
 * A request that starts while another request's challenge is still being completed
 * waits for that challenge first, so it is sent once instead of being challenged
 * itself and then retried. A request that was already in flight when a challenge
 * was completed retries without a second prompt, unless another prompt is open:
 * then it waits for that prompt too.
 *
 * @param request - The request to run and possibly retry.
 * @returns The result of the request.
 */
export declare function withTrustChallengeRetry<T>(request: () => Promise<T>): Promise<T>;
/**
 * Extracts the challenge URL from a Trust Battery `CHALLENGE_REQUIRED` GraphQL error.
 *
 * Only URLs on the Admin host are trusted. Anything else is not treated as a
 * challenge and the original error surfaces through normal error handling.
 *
 * @param error - The error thrown by an Admin API request.
 * @returns The challenge URL, or undefined when the error is not a trust challenge.
 */
export declare function trustChallengeUrl(error: unknown): Promise<string | undefined>;

Existing type declarations

packages/cli-kit/dist/public/node/ui.d.ts
@@ -375,10 +375,30 @@ export interface RenderDangerousConfirmationPromptOptions extends Omit<Dangerous
  *
  */
 export declare function renderDangerousConfirmationPrompt({ renderOptions, ...props }: RenderDangerousConfirmationPromptOptions, uiDebugOptions?: UIDebugOptions): Promise<boolean>;
-/** Waits for any key to be pressed except Ctrl+C which will terminate the process. */
+interface KeypressOptions {
+    /**
+     * The keys that end the wait, for example `['\r']` for Enter. Other keys are ignored.
+     * Without this option, any key ends the wait.
+     */
+    keys?: string[];
+}
+/**
+ * Waits for a key to be pressed. Ctrl+C rejects with AbortSilentError, which terminates the process.
+ *
+ * Raw mode is set back to the state it had before the call. A command that keeps stdin raw for
+ * its own single-key shortcuts (for example `theme dev`) keeps them.
+ *
+ * Chunks are read as text: an active Ink renderer calls `stdin.setEncoding('utf8')`, and then
+ * stdin emits strings instead of Buffers.
+ *
+ * @param stdin - The stream to read from.
+ * @param uiDebugOptions - Debug options; `skipTTYCheck` allows a non-TTY stdin.
+ * @param options - The keys that end the wait.
+ * @returns A promise that resolves when an accepted key is pressed.
+ */
 export declare const keypress: (stdin?: NodeJS.ReadStream & {
     fd: 0;
-}, uiDebugOptions?: UIDebugOptions) => Promise<unknown>;
+}, uiDebugOptions?: UIDebugOptions, { keys }?: KeypressOptions) => Promise<void>;
 interface IsTTYOptions {
     stdin?: NodeJS.ReadStream;
     uiDebugOptions?: UIDebugOptions;
packages/cli-kit/dist/public/node/themes/api.d.ts
@@ -34,6 +34,15 @@ export declare function metafieldDefinitionsByOwnerType(type: MetafieldOwnerType
     };
 }[]>;
 export declare function passwordProtected(session: AdminSession): Promise<boolean>;
+/**
+ * Whether the error ends a Trust Battery challenge on a theme request. A caller that
+ * retries theme requests must rethrow it, or the user is asked to verify again for
+ * each attempt.
+ *
+ * @param error - The error to check.
+ * @returns True when the error ends a Trust Battery challenge.
+ */
+export declare function isTrustChallengeError(error: unknown): boolean;
 type OnlineStoreThemeFileBody = {
     __typename: 'OnlineStoreThemeFileBodyBase64';
     contentBase64: string;

@iaineh
iaineh added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 659cb76 Oct 20, 2026
30 checks passed
@iaineh
iaineh deleted the gg-theme-trust-battery-cli branch October 20, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants