{"version":3,"file":"deploy-diff.mjs","names":[],"sources":["../src/deploy-diff.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { collectSlotPinRefs } from \"./rollback\";\nimport type { DistFile } from \"./storage-upload\";\nimport type { BosConfigInput } from \"./types\";\n\n/**\n * Differential bundle uploads (skip-unchanged): the previous deploy's version\n * manifest carries the dist's full per-file SRI map (`files`), so the next\n * deploy hashes its local dist, diffs against that map, and re-uploads only\n * new or changed files. Content-hashed dist filenames make an unchanged chunk\n * byte-identical, so a typical redeploy ships a handful of files instead of\n * the whole tree.\n *\n * Correctness: a skip requires (path, sha384) to match the previous\n * **server-computed** SRI map — client hashes decide only what to skip; the\n * published manifest keeps carrying the server-computed values. Any failure\n * to resolve the previous map (no pointer, fetch failed, SRI mismatch, manifest\n * predates `files`) degrades to a full upload.\n */\n\nexport interface PreviousPin {\n  manifest: string;\n  integrity: string;\n}\n\nexport interface DeployStateFile {\n  version: 1;\n  /** key: `<account>/<gateway>`, then workspace key → pin */\n  apps: Record<string, Record<string, PreviousPin>>;\n}\n\nexport const BUNDLE_BASE_PATTERN = /\\/bundles\\/([^/]+)\\/([^/]+)\\/([^/]+)\\/$/;\n\nexport function workspaceKeyOfBase(base: string): string | null {\n  const match = BUNDLE_BASE_PATTERN.exec(`${base.replace(/\\/$/, \"\")}/`);\n  return match ? match[3]! : null;\n}\n\n/** The sha384 SRI the storage route would compute for these bytes. */\nexport function localObjectIntegrity(bytes: Uint8Array): string {\n  return `sha384-${createHash(\"sha384\").update(bytes).digest(\"base64\")}`;\n}\n\nexport function localSriMap(files: DistFile[]): Record<string, string> {\n  return Object.fromEntries(files.map((file) => [file.path, localObjectIntegrity(file.bytes)]));\n}\n\nconst deployStatePath = (configDir: string) => join(configDir, \".bos\", \"deploy-state.json\");\n\nexport function readDeployState(configDir: string): DeployStateFile {\n  const path = deployStatePath(configDir);\n  if (!existsSync(path)) return { version: 1, apps: {} };\n  try {\n    const parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<DeployStateFile>;\n    if (parsed.version !== 1 || typeof parsed.apps !== \"object\" || parsed.apps === null) {\n      return { version: 1, apps: {} };\n    }\n    return { version: 1, apps: parsed.apps as DeployStateFile[\"apps\"] };\n  } catch {\n    return { version: 1, apps: {} };\n  }\n}\n\nexport function writeDeployState(configDir: string, state: DeployStateFile): void {\n  const path = deployStatePath(configDir);\n  mkdirSync(dirname(path), { recursive: true });\n  writeFileSync(path, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport function upsertDeployStatePins(\n  configDir: string,\n  account: string,\n  gateway: string,\n  pins: Record<string, PreviousPin>,\n): void {\n  const state = readDeployState(configDir);\n  const appKey = `${account}/${gateway}`;\n  state.apps[appKey] = { ...state.apps[appKey], ...pins };\n  writeDeployState(configDir, state);\n}\n\n/** Previous pins by workspace key, from the local deploy-state pointer. */\nexport function pinsFromDeployState(\n  configDir: string,\n  account: string,\n  gateway: string,\n): Record<string, PreviousPin> {\n  return readDeployState(configDir).apps[`${account}/${gateway}`] ?? {};\n}\n\n/** Previous pins by workspace key, from the published config's slot pins. */\nexport function pinsFromPublishedConfig(\n  config: BosConfigInput,\n  account: string,\n  gateway: string,\n): Record<string, PreviousPin> {\n  const pins: Record<string, PreviousPin> = {};\n  for (const ref of collectSlotPinRefs(config)) {\n    const workspace = workspaceKeyOfBase(ref.base);\n    const nsMatch = BUNDLE_BASE_PATTERN.exec(`${ref.base.replace(/\\/$/, \"\")}/`);\n    if (!workspace || !nsMatch || nsMatch[1] !== account || nsMatch[2] !== gateway) continue;\n    pins[workspace] = ref.pin;\n  }\n  return pins;\n}\n\nexport interface WorkspaceDiff {\n  /** files that must be uploaded (new or changed) */\n  upload: DistFile[];\n  /** number of files skipped because the previous manifest holds identical bytes */\n  skipped: number;\n}\n\nexport function diffWorkspaceFiles(\n  files: DistFile[],\n  localSri: Record<string, string>,\n  prevFiles: Record<string, string>,\n): WorkspaceDiff {\n  const upload: DistFile[] = [];\n  let skipped = 0;\n  for (const file of files) {\n    if (prevFiles[file.path] !== undefined && prevFiles[file.path] === localSri[file.path]) {\n      skipped += 1;\n      continue;\n    }\n    upload.push(file);\n  }\n  return { upload, skipped };\n}\n\n/**\n * The manifest's merged per-file map for this deploy: uploaded files carry the\n * server-computed SRI from the upload response; skipped files carry the\n * previous manifest's server-computed SRI (byte-identical to the local hash by\n * the skip decision). Files that left the dist carry nothing.\n */\nexport function mergedFileIntegrity(\n  files: DistFile[],\n  uploadedIntegrity: Record<string, string>,\n  prevFiles: Record<string, string> | undefined,\n): Record<string, string> {\n  const merged: Record<string, string> = {};\n  for (const file of files) {\n    const uploaded = uploadedIntegrity[file.path];\n    if (uploaded) {\n      merged[file.path] = uploaded;\n      continue;\n    }\n    const carried = prevFiles?.[file.path];\n    if (carried) merged[file.path] = carried;\n  }\n  return merged;\n}\n"],"mappings":";;;;;;AAiCA,MAAa,sBAAsB;AAEnC,SAAgB,mBAAmB,MAA6B;CAC9D,MAAM,QAAQ,oBAAoB,KAAK,GAAG,KAAK,QAAQ,OAAO,EAAE,EAAE,EAAE;CACpE,OAAO,QAAQ,MAAM,KAAM;AAC7B;;AAGA,SAAgB,qBAAqB,OAA2B;CAC9D,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,QAAQ;AACrE;AAEA,SAAgB,YAAY,OAA2C;CACrE,OAAO,OAAO,YAAY,MAAM,KAAK,SAAS,CAAC,KAAK,MAAM,qBAAqB,KAAK,KAAK,CAAC,CAAC,CAAC;AAC9F;AAEA,MAAM,mBAAmB,cAAsB,KAAK,WAAW,QAAQ,mBAAmB;AAE1F,SAAgB,gBAAgB,WAAoC;CAClE,MAAM,OAAO,gBAAgB,SAAS;CACtC,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO;EAAE,SAAS;EAAG,MAAM,CAAC;CAAE;CACrD,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;EACpD,IAAI,OAAO,YAAY,KAAK,OAAO,OAAO,SAAS,YAAY,OAAO,SAAS,MAC7E,OAAO;GAAE,SAAS;GAAG,MAAM,CAAC;EAAE;EAEhC,OAAO;GAAE,SAAS;GAAG,MAAM,OAAO;EAAgC;CACpE,QAAQ;EACN,OAAO;GAAE,SAAS;GAAG,MAAM,CAAC;EAAE;CAChC;AACF;AAEA,SAAgB,iBAAiB,WAAmB,OAA8B;CAChF,MAAM,OAAO,gBAAgB,SAAS;CACtC,UAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CAC5C,cAAc,MAAM,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,EAAE,GAAG;AAC3D;AAEA,SAAgB,sBACd,WACA,SACA,SACA,MACM;CACN,MAAM,QAAQ,gBAAgB,SAAS;CACvC,MAAM,SAAS,GAAG,QAAQ,GAAG;CAC7B,MAAM,KAAK,UAAU;EAAE,GAAG,MAAM,KAAK;EAAS,GAAG;CAAK;CACtD,iBAAiB,WAAW,KAAK;AACnC;;AAGA,SAAgB,oBACd,WACA,SACA,SAC6B;CAC7B,OAAO,gBAAgB,SAAS,CAAC,CAAC,KAAK,GAAG,QAAQ,GAAG,cAAc,CAAC;AACtE;;AAGA,SAAgB,wBACd,QACA,SACA,SAC6B;CAC7B,MAAM,OAAoC,CAAC;CAC3C,KAAK,MAAM,OAAO,mBAAmB,MAAM,GAAG;EAC5C,MAAM,YAAY,mBAAmB,IAAI,IAAI;EAC7C,MAAM,UAAU,oBAAoB,KAAK,GAAG,IAAI,KAAK,QAAQ,OAAO,EAAE,EAAE,EAAE;EAC1E,IAAI,CAAC,aAAa,CAAC,WAAW,QAAQ,OAAO,WAAW,QAAQ,OAAO,SAAS;EAChF,KAAK,aAAa,IAAI;CACxB;CACA,OAAO;AACT;AASA,SAAgB,mBACd,OACA,UACA,WACe;CACf,MAAM,SAAqB,CAAC;CAC5B,IAAI,UAAU;CACd,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,UAAU,KAAK,UAAU,UAAa,UAAU,KAAK,UAAU,SAAS,KAAK,OAAO;GACtF,WAAW;GACX;EACF;EACA,OAAO,KAAK,IAAI;CAClB;CACA,OAAO;EAAE;EAAQ;CAAQ;AAC3B;;;;;;;AAQA,SAAgB,oBACd,OACA,mBACA,WACwB;CACxB,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,kBAAkB,KAAK;EACxC,IAAI,UAAU;GACZ,OAAO,KAAK,QAAQ;GACpB;EACF;EACA,MAAM,UAAU,YAAY,KAAK;EACjC,IAAI,SAAS,OAAO,KAAK,QAAQ;CACnC;CACA,OAAO;AACT"}