1'use strict';23// The Shopify CLI connection: the required scopes, the session check and its receipt,4// the first-connection question, the staff-account record, and the re-approval request.5//6// Session check, in this order, stopping at the first failure:7// 1. Node.js runtime >= 22.12 -- before any install prompt or CLI process;8// 2. the adapter's own CLI install at exactly 4.8.3, its engines.node, `version`, and the9// `store` commands; installing only with the operator's consent;10// 3. the connection is live and Shopify's scope readback holds every required scope;11// 4. the API-version canary: Shopify lists 2026-10 as supported;12// 5. the receipt, written through a temp file and a rename.13// A receipt on disk therefore means the most recent check passed: the previous one is removed
117const FIRST_CONNECTION_QUESTION = 'Was the Shopify connection approved from a staff account the store owner created for this migration (Administrator role, or a custom role with app and channel management)?';
118const FIRST_CONNECTION_RETRY = `Ask the store owner to create a dedicated migration staff account (Administrator role, or a custom role with app and channel management), run ${CONNECT_HINT} while signed in to the store as that account in the browser, then answer again.`;
148const AUTH_EXPIRED_PROMPT = `The Shopify connection for this migration expired. Run the Shopify session check; if asked, run ${CONNECT_HINT} and approve in the browser; then answer this request to resume. Finished datasets are not read again.`;
149
150// source_auth_expired: the stopping script records a manual action with the step's own stage;
151// while it is pending the run holds at awaiting_user_action. A still-pending request is reused.
152async function recordAuthExpired(projectDir, { stage, store }) {
153 if (stage !== 'discovery' && stage !== 'execution') throw new Error('recordAuthExpired: stage must be discovery or execution');
187function confirmStaffRemoval(projectDir, domain, value, { now = () => new Date().toISOString(), io = fs } = {}) {
188 if (value !== STAFF_REMOVAL_CONFIRMATION) {
189 throw new ShopifySessionStop('shopify_staff_confirmation_invalid', `the only accepted confirmation is ${STAFF_REMOVAL_CONFIRMATION}: the store owner confirmed the migration staff account was removed. Nothing was changed.`);
190 }
191 const record = readStaffAccess(projectDir);
192 if (record.state !== 'ok') {
193 throw new ShopifySessionStop(`shopify_staff_record_${record.state}`, `${STAFF_ACCESS_FILE} is ${record.state}; ${repairHint()}, then confirm again. Nothing was changed.`);
197 throw new ShopifySessionStop('shopify_staff_entry_not_pending', `${STAFF_ACCESS_FILE} has no pending staff-account entry for ${domain}. Nothing was changed.`);
210 throw new ShopifySessionStop('shopify_staff_repair_no_evidence', `nothing in this project shows a Shopify connection for ${domain} (no passing session receipt and no answered first-connection question). Nothing was changed.`);
264 throw new ShopifySessionStop('shopify_cli_not_installed', `the adapter installs its own Shopify CLI ${cli.SHOPIFY_CLI_VERSION} into ${cli.cliInstallDir(toolsDir)} (about 52 MB, no global change). Ask the operator; with their consent run the session check again with --install. Nothing was installed or read.`);
265 }
266 throw new ShopifySessionStop('shopify_cli_version_changed', `the adapter's Shopify CLI is ${installed.version || 'unreadable'}, not the tested ${cli.SHOPIFY_CLI_VERSION}. With the operator's consent run the session check again with --install to reinstall ${cli.SHOPIFY_CLI_VERSION}. Nothing was read.`);
271 if (!installed.present) throw new ShopifySessionStop('shopify_cli_install_failed', `the install finished but ${cli.cliPackageDir(toolsDir)} holds no package.`);
272 if (installed.version !== cli.SHOPIFY_CLI_VERSION) throw new ShopifySessionStop('shopify_cli_version_changed', `the install produced Shopify CLI ${installed.version}, not ${cli.SHOPIFY_CLI_VERSION}. Nothing was read.`);
273 }
274 if (installed.enginesNode !== cli.SHOPIFY_CLI_ENGINES_NODE) {
275 throw incompatible(`the installed Shopify CLI declares engines.node ${installed.enginesNode || '(none)'}, not ${cli.SHOPIFY_CLI_ENGINES_NODE}; the pin and SHOPIFY_CLI_NODE_MIN moved apart.`);
276 }
277 const version = await cli.startCliProcess(['version'], { toolsDir, env, run });
278 if (version.error) throw incompatible('the Shopify CLI refused `version`.');
280 if (!printed.split(/\s+/).includes(cli.SHOPIFY_CLI_VERSION)) {
281 throw new ShopifySessionStop('shopify_cli_version_changed', `the Shopify CLI reports version ${printed || '(nothing)'}, not ${cli.SHOPIFY_CLI_VERSION}. Nothing was read.`);
282 }
283 const help = await cli.startCliProcess(['store', 'execute', '--help'], { toolsDir, env, run });
284 if (help.error || !/--store\b/.test(`${help.stdout}\n${help.stderr}`)) throw incompatible('the Shopify CLI has no `store` commands.');
296 return new ShopifySessionStop('shopify_session_missing', `there is no valid Shopify connection for ${domain} (${detail}). Run ${CONNECT_HINT} and approve in the browser while signed in as the dedicated migration staff account, then run the check again. Nothing was read.`);
339 throw new ShopifySessionStop('shopify_connect_failed', `the browser approval did not complete (the Shopify CLI exited ${auth.exitCode === null ? 'abnormally' : `with ${auth.exitCode}`}). Run ${CONNECT_HINT} again and approve in the browser. Nothing was read.`);
358 throw new ShopifySessionStop('shopify_scopes_missing', `Shopify did not grant ${missing.join(', ')}. Run ${CONNECT_HINT} (it requests the full list) and approve in the browser, then run the check again. Nothing was read.`, { missingScopes: missing, grantedScopes: granted });
359 }
360 // 4. The API-version canary.
361 const canary = await execute(CANARY_QUERY);
362 if (canary.errors && canary.errors.length) {
363 throw new ShopifySessionStop('shopify_canary_failed', `the API-version canary read failed: ${cli.errorCodes(canary.errors).join(', ')}. Nothing was read.`);
368 throw new ShopifySessionStop('shopify_api_version_unsupported', `Shopify ${pinned ? 'lists' : 'does not list'} the pinned Admin API version ${cli.SHOPIFY_ADMIN_API_VERSION}${pinned ? ' as unsupported' : ''}. Moving the pin is its own tested change. Nothing was read.`);
388// Discovery and the extract start only when the latest first-connection answer is
389// `dedicated-account` and the receipt passes for this domain.
390async function assertConnectionReady(projectDir, store) {
391 const domain = normalizeDomain(store);
392 const state = await firstConnectionState(projectDir, domain);
393 if (state.answer !== 'dedicated-account') {
394 throw new ShopifySessionStop('shopify_first_connection_unconfirmed', `the Shopify connection for ${domain} is not confirmed as approved from a dedicated migration staff account${state.latest ? ` (request ${state.latest.requestId} is ${state.latest.status === 'pending' ? 'pending' : `answered ${state.answer}`})` : ''}. Run the Shopify session check and answer its first-connection question. Nothing was read.`);
397 if (!passingReceipt(receipt) || receipt.shopDomain !== domain) {
398 throw new ShopifySessionStop('shopify_session_not_checked', `no passing Shopify session check for ${domain} is recorded. Run \`node scripts/${SESSION_CHECK} --project <project>\` first. Nothing was read.`);