{"version":3,"file":"preflight.mjs","names":[],"sources":["../../../src/services/deploy/preflight.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\nimport { selectWorkspaceTargets } from \"../../services/workspace/build-train\";\nimport type { BosConfig, PublishConfig } from \"../../types\";\nimport { fetchResponse } from \"../../utils/http-client\";\nimport { getNetworkIdForAccount } from \"../../utils/network\";\nimport { colors } from \"../../utils/theme\";\nimport {\n  describeSigningStrategy,\n  resolveSigningStrategy,\n  type SigningStrategy,\n} from \"../auth/near-signer\";\nimport type { CustodyWalletRecord, SessionCredential } from \"../auth/session\";\nimport { readSessionHandle } from \"../auth/session\";\nimport {\n  buildRegistryConfigUrlForNetwork,\n  fetchPublishedConfigFromFastKv,\n  getRegistryNamespaceForNetwork,\n  type NetworkId,\n} from \"../registry/fastkv\";\nimport { type CdnDeployInputs, probeStorageOrigin, resolveCdnDeployInputs } from \"./cdn\";\n\n/**\n * Pure publish preflight (resolve, don't mutate) — every failure returns\n * before the build train runs (issue #287). Dry-run exits after the auth\n * guards, before any signing (keychain/TTY side effects).\n *\n * The validated auth handles are captured on the plan (guards once per run,\n * ticket #422): the train's submit phase consumes `plan.custodyWallet` /\n * `plan.credential` instead of re-reading and re-validating the session file.\n */\n\nexport type PublishedConfigState = \"fetched\" | \"missing\" | \"unreachable\";\n\nexport interface PublishPreflightPlan {\n  isStaging: boolean;\n  account: string;\n  gateway: string;\n  network: NetworkId;\n  registryUrl: string;\n  registryNamespace: string;\n  targets: string[];\n  useWallet: boolean;\n  useCustody: boolean;\n  strategy?: SigningStrategy;\n  cdnOrigin: string | undefined;\n  storageOrigin: string;\n  storageApiKey?: string;\n  /** The config currently published to FastKV (null when missing or unreachable). */\n  publishedConfig: Record<string, unknown> | null;\n  /** Whether the preflight FastKV read succeeded, found nothing, or failed. */\n  publishedConfigState: PublishedConfigState;\n  /** Validated by the custody guard — present iff useCustody. */\n  custodyWallet?: CustodyWalletRecord;\n  /** Validated by the session guard — present iff useWallet. */\n  credential?: SessionCredential;\n}\n\nexport type PublishPreflight =\n  | { kind: \"dry-run\"; registryUrl: string }\n  | { kind: \"error\"; registryUrl: string; error: string }\n  | { kind: \"ready\"; plan: PublishPreflightPlan };\n\nconst FRAMEWORK_PACKAGE_NAMES = [\"better-near-auth\", \"every-plugin\", \"everything-dev\"] as const;\n\n/**\n * Whether npm serves `name@version` (true), definitively does not (404 —\n * false), or whether publishability could not be determined (network error,\n * unexpected status — null; callers must not block on that).\n */\nexport async function npmVersionPublished(name: string, version: string): Promise<boolean | null> {\n  try {\n    const res = await fetchResponse(`https://registry.npmjs.org/${name}/${version}`, {\n      headers: { Accept: \"application/json\" },\n    });\n    if (res.status === 200) return true;\n    if (res.status === 404) return false;\n    return null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * The npm-publish gate: a workspace that vendors the framework packages'\n * sources must not ship bundles ahead of npm — the update check, child\n * scaffolds (`bos init`), and template syncs all read those versions from\n * the registry. Reads only apply when the sources are workspace-local (the\n * parent repo); child projects install the framework from npm and are\n * inherently in sync.\n */\nexport async function findUnpublishedFrameworkPackages(\n  configDir: string,\n): Promise<Array<{ name: string; version: string }>> {\n  const unpublished: Array<{ name: string; version: string }> = [];\n  for (const name of FRAMEWORK_PACKAGE_NAMES) {\n    const manifestPath = join(configDir, \"packages\", name, \"package.json\");\n    if (!existsSync(manifestPath)) continue;\n    const version = (JSON.parse(readFileSync(manifestPath, \"utf-8\")) as { version?: string })\n      .version;\n    if (!version) continue;\n    const published = await npmVersionPublished(name, version);\n    if (published === false) {\n      unpublished.push({ name, version });\n    } else if (published === null) {\n      console.log(\n        colors.yellow(\n          `  ! Could not verify ${name}@${version} on npm (registry unreachable) — continuing`,\n        ),\n      );\n    }\n  }\n  return unpublished;\n}\n\ninterface PreflightInput {\n  bosConfig: BosConfig;\n  configDir: string;\n  env: \"production\" | \"staging\";\n  build: boolean;\n  dryRun: boolean;\n  packages: string;\n  network?: \"mainnet\" | \"testnet\";\n  privateKey?: string;\n  wallet?: boolean;\n  registry?: string;\n}\n\nexport async function preflightPublish(input: PreflightInput): Promise<PublishPreflight> {\n  const { configDir } = input;\n  const bosConfig = input.bosConfig;\n\n  const isStaging = input.env === \"staging\";\n  const account = isStaging ? (bosConfig.staging?.account ?? bosConfig.account) : bosConfig.account;\n  const gateway = isStaging ? (bosConfig.staging?.domain ?? bosConfig.domain) : bosConfig.domain;\n  if (!gateway) {\n    return {\n      kind: \"error\",\n      registryUrl: \"\",\n      error: \"authored config must define domain to publish\",\n    };\n  }\n\n  const network: NetworkId = input.network ?? getNetworkIdForAccount(account);\n  const registryUrl = buildRegistryConfigUrlForNetwork(network, account, gateway, input.registry);\n  const fail = (error: string): PublishPreflight => ({ kind: \"error\", registryUrl, error });\n\n  // One session-file read for the whole run (guards once, ticket #422): the\n  // wallet/custody guards validate + capture their handles on the plan, and\n  // CDN credential derivation consumes the raw credential as before.\n  const sessionFile = readSessionHandle(configDir);\n  const publishAuth: PublishConfig[\"auth\"] = bosConfig.publish?.auth;\n  const governsWalletPublish = (publishAuth === \"session\" || input.wallet) && !input.privateKey;\n  let credential: SessionCredential | undefined;\n  if (governsWalletPublish) {\n    if (!sessionFile?.credential) {\n      return fail(\n        (input.wallet\n          ? \"--wallet requires\"\n          : 'the authored config sets publish.auth = \"session\", but') +\n          \" no CLI session is stored in .bos/ for this project. Run bos login to create one.\",\n      );\n    }\n    if (sessionFile.credential.accountId && sessionFile.credential.accountId !== account) {\n      return fail(\n        `The CLI session was created for ${sessionFile.credential.accountId}, but the configured ` +\n          `account is ${account}. Gasless wallet publish relays the FastKV write under the session's ` +\n          \"NEAR account. Run bos login again under the matching account.\",\n      );\n    }\n    credential = sessionFile.credential;\n  }\n  const useCustody = publishAuth === \"custody\" && !input.privateKey && !input.wallet;\n  let custodyWallet: CustodyWalletRecord | undefined;\n  if (useCustody) {\n    const wallet = sessionFile?.custodyWallet;\n    if (!wallet) {\n      return fail(\n        'the authored config sets publish.auth = \"custody\", but no custody wallet is stored in ' +\n          \".bos/. Run `bos login --custody` first.\",\n      );\n    }\n    if (wallet.network !== network) {\n      return fail(\n        `The custody wallet was registered on ${wallet.network}, but this project publishes ` +\n          `on ${network}. Run \\`bos login --custody\\` from a ${network} account.`,\n      );\n    }\n    if (wallet.nearAccountId !== account) {\n      return fail(\n        `publish.auth = \"custody\" publishes under the custody wallet's account, but the configured ` +\n          `account is ${account} and the custody wallet account is ${wallet.nearAccountId}. ` +\n          \"Set the authored config's account to the custody wallet account.\",\n      );\n    }\n    custodyWallet = wallet;\n  }\n\n  if (input.dryRun) {\n    return { kind: \"dry-run\", registryUrl };\n  }\n\n  const useWallet = input.wallet === true;\n  let strategy: SigningStrategy | undefined;\n  if (useWallet) {\n    console.log(\n      `  Signing via ${colors.cyan(\"gasless NEP-366 delegate action (relayed by the platform relayer)\")}`,\n    );\n  } else {\n    try {\n      strategy = await resolveSigningStrategy({ privateKey: input.privateKey, account, network });\n      console.log(`  Signing via ${colors.cyan(describeSigningStrategy(strategy))}`);\n    } catch (error) {\n      return fail(error instanceof Error ? error.message : \"Unknown error\");\n    }\n  }\n\n  // CDN deploy credentials (ADR 0020) — resolved before the build train so a\n  // missing bundle origin or BOS_STORAGE_API_KEY fails fast instead of after\n  // the full build. Config-only publishes ship the committed config verbatim;\n  // nothing is uploaded, so no deploy origins are required.\n  let cdnDeploy: CdnDeployInputs = {\n    cdnOrigin: undefined,\n    storageOrigin: \"\",\n    apiKey: undefined,\n  };\n  if (input.build) {\n    const unpublished = await findUnpublishedFrameworkPackages(configDir);\n    if (unpublished.length > 0) {\n      const listing = unpublished.map((pkg) => `${pkg.name}@${pkg.version}`).join(\", \");\n      return fail(\n        `Framework package(s) ${listing} are not published to npm yet — deploys must not ship ` +\n          \"bundles ahead of the registry (child scaffolds and update checks read these versions). \" +\n          \"Re-run the Release workflow (or `pnpm run release`) first.\",\n      );\n    }\n\n    cdnDeploy = resolveCdnDeployInputs({\n      env: process.env as Record<string, string | undefined>,\n      bosConfig,\n      session: sessionFile?.credential ?? null,\n      custodyAccount: useCustody ? custodyWallet?.nearAccountId : undefined,\n      account,\n      gateway,\n      uploadsPlanned: input.build,\n    });\n    if (cdnDeploy.error) {\n      return fail(cdnDeploy.error);\n    }\n    if (cdnDeploy.warning) {\n      console.log(colors.yellow(`  ! ${cdnDeploy.warning}`));\n    }\n    const cdnOrigin = cdnDeploy.cdnOrigin;\n    if (!cdnOrigin) {\n      return fail(\"CDN deploy resolved no bundle origin\");\n    }\n    const probeError = await probeStorageOrigin(cdnDeploy.storageOrigin);\n    if (probeError) {\n      return fail(\n        `The bundle upload origin ${cdnDeploy.storageOrigin} is not running the platform API — ` +\n          `${probeError}. Check BOS_STORAGE_ORIGIN / the bos login session, then re-run.`,\n      );\n    }\n  }\n\n  // Registry reachability + published-state capture: one FastKV read. A\n  // missing config is fine for a first publish — an early signal, not a hard\n  // gate. The captured config is the merge base for a config-only publish on\n  // a TS-form project (authored hand-edits win; live pipeline state carries).\n  let publishedConfig: Record<string, unknown> | null = null;\n  let publishedConfigState: PublishedConfigState = \"missing\";\n  try {\n    publishedConfig = await fetchPublishedConfigFromFastKv<Record<string, unknown>>(registryUrl);\n    publishedConfigState = \"fetched\";\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error);\n    if (message.startsWith(\"No config found\")) {\n      console.log(colors.dim(\"  Note: no published config found yet (first publish) — continuing\"));\n    } else {\n      publishedConfigState = \"unreachable\";\n      console.log(colors.dim(\"  Note: registry read failed — continuing\"));\n    }\n  }\n\n  return {\n    kind: \"ready\",\n    plan: {\n      isStaging,\n      account,\n      gateway,\n      network,\n      registryUrl,\n      registryNamespace: getRegistryNamespaceForNetwork(network, input.registry),\n      targets: selectWorkspaceTargets(input.packages, bosConfig),\n      useWallet,\n      useCustody,\n      strategy,\n      cdnOrigin: cdnDeploy.cdnOrigin,\n      storageOrigin: cdnDeploy.storageOrigin,\n      storageApiKey: cdnDeploy.apiKey,\n      publishedConfig,\n      publishedConfigState,\n      ...(custodyWallet ? { custodyWallet } : {}),\n      ...(credential ? { credential } : {}),\n    },\n  };\n}\n"],"mappings":"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"}