run402 4.105.5 → 4.106.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -206,6 +206,7 @@ export const COMMAND_MANIFEST = [
206
206
  { path: ["transfer", "preview"], positionals: [p("transfer_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["tr_gate1"] },
207
207
  { path: ["transfer", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
208
208
  { path: ["transfer", "accept"], positionals: [p("transfer_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["tr_gate1"] },
209
+ { path: ["transfer", "handover"], positionals: [p("transfer_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["tr_gate1"] },
209
210
  { path: ["transfer", "cancel"], positionals: [p("transfer_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["tr_gate1"] },
210
211
 
211
212
  // ── orgs ──────────────────────────────────────────────────────────────────
@@ -310,6 +311,7 @@ export const COMMAND_MANIFEST = [
310
311
  { path: ["repos", "access"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the live org encryption-key directory + vault envelope recipients" },
311
312
  { path: ["repos", "access", "repair"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["--recipient-state-version", "0", "--recipient-revocation-version", "0"], runStyle: "sub", skipBehavioral: "owner+step-up-gated epoch rotation (D193-D203, rev 42) — samples a fresh epoch key and re-seals a live vault's recipients" },
312
313
  { path: ["repos", "access", "revoke-key"], positionals: [p("principal_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["prin_00000000000000000000000000000000"], runStyle: "sub", skipBehavioral: "owner+step-up-gated: declares a recipient's key revoked (org-scoped watermark) and drives a real epoch rotation off it" },
314
+ { path: ["repos", "access", "retire-previous-owner"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "owner+step-up-gated: blocks the previous owner's writer keys after a project transfer and drives a real epoch rotation" },
313
315
  { path: ["repos", "access", "declare-exposure"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "owner+step-up-gated: declares this vault's epoch secret exposed, forcing every subsequent ordinary push to refuse until a rotation lands" },
314
316
  { path: ["repos", "access", "sync"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "vault-multi-writer (rev 47): on-demand writer-admission reconcile — resolves pending candidates' signing keys via the live org encryption-key directory and publishes a real add_writer_key head per candidate" },
315
317
  { path: ["repos", "recover"], positionals: [p("source")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["s3://example-mirror-bucket", "--out", "__SCRATCH_DIR__/recover-out"], runStyle: "sub", skipBehavioral: "materializes a git repository from a mirror source, offline, with no server call" },
package/lib/repos.mjs CHANGED
@@ -107,6 +107,7 @@ Maintenance:
107
107
  run402 repos access [--project <project_id>] [--repo <repo_id>] [--human]
108
108
  run402 repos access repair [--project <project_id>] [--repo <repo_id>] --recipient-state-version <n> --recipient-revocation-version <n>
109
109
  run402 repos access revoke-key <principal_id> [--project <project_id>] [--repo <repo_id>]
110
+ run402 repos access retire-previous-owner [--project <project_id>] [--repo <repo_id>]
110
111
  run402 repos access declare-exposure [--project <project_id>] [--repo <repo_id>]
111
112
  run402 repos access repin [--project <project_id>] [--repo <repo_id>] --principal <principal_id> --fingerprint <ek_fingerprint>
112
113
  run402 repos policy <required|grandfathered> [--project <project_id>] [--repo <repo_id>] [--reason <why>]
@@ -358,6 +359,13 @@ Subcommands:
358
359
  that declaration's OWN returned counters — no flags needed.
359
360
  Owner + step-up. The rekey remedy for "this specific principal's
360
361
  key should no longer be trusted."
362
+ access retire-previous-owner
363
+ After a project transfer: blocks every writer key whose principal
364
+ is not a developer+ member of the vault's organization (the
365
+ previous owner's writers) and commits the writer_key_revoked
366
+ rotation that removes them. A transfer never does this on its own.
367
+ Protects only what is written afterwards. Owner + step-up. Safe to
368
+ re-run; nothing to retire means no rotation.
361
369
  access declare-exposure
362
370
  Declares reason:"epoch_secret_exposed" for THIS vault
363
371
  (vault-scoped, not org-wide) — the rekey remedy for a leaked
@@ -2941,6 +2949,30 @@ async function accessRevokeKey(args) {
2941
2949
  * fingerprint (the out-of-band verification point) and moves the local pin.
2942
2950
  * Adapter only: `sdk.repos.acceptRecipientKeyChange`.
2943
2951
  */
2952
+ async function accessRetirePreviousOwner(args) {
2953
+ const a = normalizeArgv(args);
2954
+ assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--idempotency-key", "-v", "--verbose", "--help", "-h"], [...COMMON_VALUE_FLAGS, "--idempotency-key"]);
2955
+ requirePositionalCount(a, [...COMMON_VALUE_FLAGS, "--idempotency-key"], { min: 0, max: 0, command: "run402 repos access retire-previous-owner", missing: "" });
2956
+ const sdk = getSdk();
2957
+ const target = await vaultTarget(a);
2958
+ try {
2959
+ const result = await sdk.repos.retirePreviousOwnerWriters({
2960
+ ...target,
2961
+ ...(flagValue(a, "--idempotency-key") != null ? { client_idempotency_key: flagValue(a, "--idempotency-key") } : {}),
2962
+ });
2963
+ printJson(sdk, result);
2964
+ if (result.retired.length === 0) {
2965
+ console.error("no writer outside this vault's organization — nothing to retire.");
2966
+ } else {
2967
+ console.error(`retired ${result.retired.length} writer key(s) from outside the organization and rotated to epoch ${result.rotation?.new_epoch} at generation ${result.rotation?.generation}.`);
2968
+ }
2969
+ console.error(result.note);
2970
+ printVerboseStats(a, sdk);
2971
+ } catch (err) {
2972
+ reportSdkError(err);
2973
+ }
2974
+ }
2975
+
2944
2976
  async function accessRepin(args) {
2945
2977
  const a = normalizeArgv(args);
2946
2978
  assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--principal", "--fingerprint", "--help", "-h"], [...COMMON_VALUE_FLAGS, "--principal", "--fingerprint"]);
@@ -3019,6 +3051,7 @@ async function access(args) {
3019
3051
  const a = normalizeArgv(args);
3020
3052
  if (a[0] === "repair") return accessRepair(a.slice(1));
3021
3053
  if (a[0] === "revoke-key") return accessRevokeKey(a.slice(1));
3054
+ if (a[0] === "retire-previous-owner") return accessRetirePreviousOwner(a.slice(1));
3022
3055
  if (a[0] === "declare-exposure") return accessDeclareExposure(a.slice(1));
3023
3056
  if (a[0] === "repin") return accessRepin(a.slice(1));
3024
3057
  if (a[0] === "sync") return accessSync(a.slice(1));
package/lib/transfer.mjs CHANGED
@@ -18,6 +18,7 @@ Usage:
18
18
  run402 transfer preview <transfer_id>
19
19
  run402 transfer list [--incoming | --outgoing] [--limit N] [--after <cursor>]
20
20
  run402 transfer accept <transfer_id> [--org <org_id>] [--accept-retained-member]
21
+ run402 transfer handover <transfer_id>
21
22
  run402 transfer cancel <transfer_id> [--reason <text>]
22
23
 
23
24
  Subcommands:
@@ -28,7 +29,10 @@ Subcommands:
28
29
  list List pending transfers (incoming default, or --outgoing) — pending rows unioned
29
30
  accept Accept an incoming transfer, whatever its address (wallet: your wallet must be
30
31
  the to_wallet; email: your verified email must match; --org <org_id> picks the
31
- receiving org, omit = new org)
32
+ receiving org, omit = new org). For a project with a vault, accept first
33
+ names you as the recipient of its source and waits for the sender's handover
34
+ handover Sender: hand the project's vault to the recipient they named (admit their key
35
+ as a writer and wrap the source key for them), so they can accept
32
36
  cancel Cancel a pending transfer of any kind
33
37
 
34
38
  Notes:
@@ -38,6 +42,10 @@ Notes:
38
42
  - --to-org is same-actor only in the first gateway release: you must own both orgs.
39
43
  - Secret VALUES are inherited by the recipient on completion; rotation is advised.
40
44
  - GitHub repo ownership is NOT transferred — handle that out of band.
45
+ - A project's KyGit vault moves with it. The recipient cannot accept until the sender
46
+ runs 'transfer handover'; afterwards the new owner retires the previous owner's
47
+ writers with 'run402 repos access retire-previous-owner' when ready.
48
+ - A project whose vault stores its source in your own bucket (BYO) is not transferable.
41
49
  `;
42
50
 
43
51
  const SUB_HELP = {
@@ -101,10 +109,27 @@ owner's CI bindings on the project, enqueues notifications to both parties,
101
109
  stamps a 'secrets_rotation_advised' advisory on the project, and returns the
102
110
  project keys (saved to your keystore).
103
111
 
112
+ For a project with a KyGit vault, the source is handed over before accept: on
113
+ the first run accept publishes this machine's keystore identity, names you as the
114
+ principal that receives the source, and stops with TRANSFER_VAULT_HANDOVER_PENDING
115
+ until the sender runs 'run402 transfer handover <transfer_id>'. Run accept again
116
+ once 'transfer preview' shows vault_handover.state = complete.
117
+
104
118
  Options:
105
119
  --org <org_id> Email-addressed only: the org to receive the project (omit = new org).
106
120
  --accept-retained-member Email-addressed only: accept the sender's retained-developer-membership
107
121
  offer (see 'transfer preview' retain_member). Omit = full severance.
122
+ `,
123
+ handover: `run402 transfer handover — Hand a transferred project's vault to the recipient
124
+
125
+ Usage:
126
+ run402 transfer handover <transfer_id>
127
+
128
+ The sender's step. Reads the principal the recipient named (they run 'transfer
129
+ accept' first), admits that principal's own published signing key as a writer of
130
+ the project's vault, and wraps the vault's current key for it. Your key must be a
131
+ writer of the vault. Safe to re-run: a step already done is reported as such.
132
+ The recipient can accept once vault_handover.state is complete.
108
133
  `,
109
134
  cancel: `run402 transfer cancel — Cancel a pending transfer
110
135
 
@@ -307,12 +332,57 @@ async function accept(args) {
307
332
  const acceptRetain = parsedArgs.includes("--accept-retained-member");
308
333
  walletAuthHeaders(`/agent/v1/transfers/${transferId}/accept`);
309
334
 
335
+ const sdk = getSdk();
336
+ const doAccept = () => sdk.admin.transfers.accept(transferId, {
337
+ orgId: orgId ?? undefined,
338
+ acceptRetainedMember: acceptRetain || undefined,
339
+ });
310
340
  try {
311
- const data = await getSdk().admin.transfers.accept(transferId, {
312
- orgId: orgId ?? undefined,
313
- acceptRetainedMember: acceptRetain || undefined,
314
- });
341
+ const data = await doAccept();
315
342
  console.log(JSON.stringify(data, null, 2));
343
+ } catch (err) {
344
+ // A project with a vault waits on its handover. When no recipient has
345
+ // been named yet, name this principal (publishing its keystore identity
346
+ // first) so the sender's 'transfer handover' has a key to hand over to.
347
+ const handoverState = err?.code === "TRANSFER_VAULT_HANDOVER_PENDING" ? err?.details?.vault_handover?.state : null;
348
+ if (handoverState !== "awaiting_recipient") {
349
+ reportSdkError(err);
350
+ return;
351
+ }
352
+ try {
353
+ await sdk.repos.publishKeystoreIdentity();
354
+ const nominated = await sdk.admin.transfers.nominateVaultRecipient(transferId);
355
+ if (nominated.vault_handover?.state === "complete") {
356
+ console.log(JSON.stringify(await doAccept(), null, 2));
357
+ return;
358
+ }
359
+ fail({
360
+ code: "TRANSFER_VAULT_HANDOVER_PENDING",
361
+ message: "You are named as the recipient of this project's source. The sender must run 'run402 transfer handover " + transferId + "' (they have been notified); then run this accept again.",
362
+ details: { transfer_id: transferId, vault_handover: nominated.vault_handover },
363
+ next_actions: nominated.next_actions,
364
+ retryable: true,
365
+ });
366
+ } catch (inner) {
367
+ reportSdkError(inner);
368
+ }
369
+ }
370
+ }
371
+
372
+ async function handover(args) {
373
+ const parsedArgs = normalizeArgv(args);
374
+ assertKnownFlags(parsedArgs, ["--help", "-h"], []);
375
+ const positionals = positionalArgs(parsedArgs, []);
376
+ if (positionals.length !== 1) {
377
+ fail({ code: "BAD_USAGE", message: "Usage: run402 transfer handover <transfer_id>" });
378
+ }
379
+ const transferId = positionals[0];
380
+ try {
381
+ const data = await getSdk().repos.completeTransferHandover(transferId);
382
+ console.log(JSON.stringify(data, null, 2));
383
+ if (data.vault_handover?.state === "complete") {
384
+ console.error(`handed over: the recipient can now run 'run402 transfer accept ${transferId}'.`);
385
+ }
316
386
  } catch (err) {
317
387
  reportSdkError(err);
318
388
  }
@@ -361,6 +431,9 @@ export async function run(sub, args) {
361
431
  case "accept":
362
432
  await accept(args);
363
433
  return;
434
+ case "handover":
435
+ await handover(args);
436
+ return;
364
437
  case "cancel":
365
438
  await cancel(args);
366
439
  return;
package/lib/up.mjs CHANGED
@@ -65,7 +65,8 @@ Options:
65
65
  commits directly, and a plan whose migrations are all
66
66
  already applied with an identical checksum is not
67
67
  rehearsed either (result.deploy.rehearsal says which:
68
- no_live_release / migrations_unchanged / no_migrations).
68
+ no_live_release / migrations_unchanged / no_migrations /
69
+ ci_session_unsupported).
69
70
  --repo-only Provision + scaffold the run402 remote + first push,
70
71
  and stop there — no deploy. The vault-only track
71
72
  (D8), composed through up instead of run402 repos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.105.5",
3
+ "version": "4.106.0",
4
4
  "description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"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"}
@@ -24,7 +24,7 @@ import { isGrantKeyCredentials } from "../grant-key-credentials.js";
24
24
  import { isKnownBinaryContent, isUtf8StringSource } from "../validation.js";
25
25
  import { assertCiDeployableSpec } from "./ci.js";
26
26
  import { ROUTE_PRICING_NETWORKS, ROUTE_HTTP_METHODS, isModernPlanMigrationDiff, normalizeDeployResolveRequest, } from "./deploy.types.js";
27
- import { ApiError, LocalError, NetworkError, PaymentRequired, Run402DeployError, Unauthorized, isTransferFreezeError, } from "../errors.js";
27
+ import { ApiError, LocalError, NetworkError, PaymentRequired, Run402DeployError, Unauthorized, isRun402Error, isTransferFreezeError, } from "../errors.js";
28
28
  import { assertAssetMetadata, assertExifPolicy, } from "./assets-validation.js";
29
29
  // ─── Constants ───────────────────────────────────────────────────────────────
30
30
  const PLAN_BODY_LIMIT_BYTES = 5 * 1024 * 1024;
@@ -36,6 +36,10 @@ const APPLY_RETRY_DEFAULT_MAX_RETRIES = 2;
36
36
  const APPLY_RETRY_BASE_DELAY_MS = 250;
37
37
  const APPLY_RETRY_MAX_DELAY_MS = 2_000;
38
38
  const APPLY_RETRY_JITTER_MS = 100;
39
+ const CONTENT_COMMIT_MAX_ATTEMPTS = 3;
40
+ const DEPLOY_COMMIT_MAX_ATTEMPTS = 3;
41
+ const DEPLOY_COMMIT_RETRY_BASE_DELAY_MS = 1_000;
42
+ const CONTENT_COMMIT_RETRY_BASE_DELAY_MS = 500;
39
43
  const URL_REFRESH_AT_MS = 50 * 60 * 1000;
40
44
  const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
41
45
  const APPLY_SAFE_RETRY_CODES = new Set([
@@ -568,11 +572,9 @@ async function rehearseBeforeCommit(client, plan, planId, project, opts, emit) {
568
572
  const envelope = plan.rehearsal;
569
573
  if (!envelope || !envelope.available) {
570
574
  const reason = envelope?.reason ?? "no_migrations";
571
- return skip(reason === "no_live_release"
572
- ? "no_live_release"
573
- : reason === "migrations_unchanged"
574
- ? "migrations_unchanged"
575
- : "no_migrations");
575
+ return skip(reason === "no_live_release" || reason === "migrations_unchanged" || reason === "ci_session_unsupported"
576
+ ? reason
577
+ : "no_migrations");
576
578
  }
577
579
  // Named migrations that are all checksum-identical noops (already applied
578
580
  // to the live project) would run NOTHING on the branch, so a rehearsal
@@ -731,7 +733,7 @@ async function applyOnce(client, spec, opts, emit) {
731
733
  apply_plan_sha256: plan.vault?.apply_plan_sha256 ?? null,
732
734
  })
733
735
  : undefined;
734
- const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, requiredPlan, vaultCommit), "applying deploy");
736
+ const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, requiredPlan, vaultCommit, operationId), "applying deploy");
735
737
  const result = await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project_id, sliceKinds);
736
738
  result.rehearsal = rehearsal.block;
737
739
  // v1.48 unified-apply: thread the plan response's `asset_entries[]` back
@@ -1591,7 +1593,7 @@ function expandCronNumberPart(part, min, max) {
1591
1593
  }
1592
1594
  return values;
1593
1595
  }
1594
- async function commitInternal(client, planId, idempotencyKey, project, requiredPlan, vault) {
1596
+ async function commitInternal(client, planId, idempotencyKey, project, requiredPlan, vault, operationId) {
1595
1597
  try {
1596
1598
  const body = {};
1597
1599
  if (idempotencyKey)
@@ -1600,7 +1602,7 @@ async function commitInternal(client, planId, idempotencyKey, project, requiredP
1600
1602
  body.required_plan = requiredPlanToWire(requiredPlan);
1601
1603
  if (vault)
1602
1604
  body.vault = vault;
1603
- return await client.request(`/apply/v1/plans/${encodeURIComponent(planId)}/commit`, {
1605
+ const send = () => client.request(`/apply/v1/plans/${encodeURIComponent(planId)}/commit`, {
1604
1606
  method: "POST",
1605
1607
  body,
1606
1608
  // Write-approval scope: committing a deploy is `project.deploy` on this project.
@@ -1615,11 +1617,43 @@ async function commitInternal(client, planId, idempotencyKey, project, requiredP
1615
1617
  : {}),
1616
1618
  context: "committing deploy",
1617
1619
  });
1620
+ return await sendCommitRecoveringLostResponse(client, send, operationId, project);
1618
1621
  }
1619
1622
  catch (err) {
1620
1623
  throw translateDeployError(err, "commit", planId, null);
1621
1624
  }
1622
1625
  }
1626
+ /**
1627
+ * Send a deploy commit, recovering when a 502/503/504 or a dropped connection
1628
+ * loses the response. The commit may still have reached the gateway (a long
1629
+ * commit can outlive the connection that carried it), and a second commit
1630
+ * while the first is running would drive the state machine twice. So read the
1631
+ * operation first: once it has left `planning`/`uploading` the commit landed,
1632
+ * and the caller follows the operation; otherwise the commit never arrived and
1633
+ * is sent again. Without an operation id to read, the failure surfaces as is.
1634
+ */
1635
+ async function sendCommitRecoveringLostResponse(client, send, operationId, projectId) {
1636
+ for (let attempt = 1;; attempt++) {
1637
+ try {
1638
+ return await send();
1639
+ }
1640
+ catch (err) {
1641
+ if (!operationId || !projectId || !isTransientGatewayFailure(err) || attempt >= DEPLOY_COMMIT_MAX_ATTEMPTS)
1642
+ throw err;
1643
+ await sleep(DEPLOY_COMMIT_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
1644
+ let snapshot;
1645
+ try {
1646
+ snapshot = await client.request(`/apply/v1/operations/${encodeURIComponent(operationId)}`, { headers: await apikeyHeaders(client, projectId), context: "fetching deploy operation" });
1647
+ }
1648
+ catch {
1649
+ throw err;
1650
+ }
1651
+ if (snapshot.status !== "planning" && snapshot.status !== "uploading") {
1652
+ return { operation_id: operationId, status: "running" };
1653
+ }
1654
+ }
1655
+ }
1656
+ }
1623
1657
  async function uploadMissing(client, projectId, presence, byteReaders, emit) {
1624
1658
  // Surface CAS dedup hits so agents can distinguish "N files were already
1625
1659
  // present" from "nothing happened". The gateway reports both present and
@@ -1692,7 +1726,7 @@ async function uploadMissing(client, projectId, presence, byteReaders, emit) {
1692
1726
  // Plan-level finalize — marks the plan committed in the deploy_plans
1693
1727
  // table. Per-session promotion to CAS already happened in the loop
1694
1728
  // above; this call is the plan-level idempotency anchor.
1695
- await client.request(`/content/v1/plans/${encodeURIComponent(planRes.plan_id)}/commit`, { method: "POST", headers, body: {}, context: "committing content upload" });
1729
+ await commitContentPlanWithRetry(client, planRes.plan_id, headers);
1696
1730
  }
1697
1731
  async function uploadCoreContent(client, projectId, byteReaders, emit) {
1698
1732
  const entries = [...byteReaders.entries()].sort(([a], [b]) => a.localeCompare(b));
@@ -1721,6 +1755,35 @@ async function uploadCoreContent(client, projectId, byteReaders, emit) {
1721
1755
  });
1722
1756
  }
1723
1757
  }
1758
+ /**
1759
+ * Finalize a content plan, riding out a transient gateway failure. A 502/503/504
1760
+ * or a dropped connection means the load balancer or the network lost the
1761
+ * request, not that the gateway refused it, and the commit is safe to repeat: the
1762
+ * gateway re-promotes the same staged sessions. If a lost attempt had in fact
1763
+ * committed, the repeat answers 409 PLAN_ALREADY_COMMITTED, which is success.
1764
+ */
1765
+ async function commitContentPlanWithRetry(client, planId, headers) {
1766
+ for (let attempt = 1;; attempt++) {
1767
+ try {
1768
+ await client.request(`/content/v1/plans/${encodeURIComponent(planId)}/commit`, { method: "POST", headers, body: {}, context: "committing content upload" });
1769
+ return;
1770
+ }
1771
+ catch (err) {
1772
+ if (attempt > 1 && isRun402Error(err) && err.status === 409 && err.code === "PLAN_ALREADY_COMMITTED")
1773
+ return;
1774
+ if (!isTransientGatewayFailure(err) || attempt >= CONTENT_COMMIT_MAX_ATTEMPTS)
1775
+ throw err;
1776
+ await sleep(CONTENT_COMMIT_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
1777
+ }
1778
+ }
1779
+ }
1780
+ function isTransientGatewayFailure(err) {
1781
+ if (!isRun402Error(err))
1782
+ return false;
1783
+ if (err.kind === "network_error")
1784
+ return true;
1785
+ return err.status === 502 || err.status === 503 || err.status === 504;
1786
+ }
1724
1787
  async function uploadOneWithRetry(fetchFn, session, bytes) {
1725
1788
  const MAX_ATTEMPTS = 3;
1726
1789
  for (let attempt = 1;; attempt++) {
@@ -2100,8 +2163,8 @@ async function startInternal(client, spec, opts) {
2100
2163
  status: "started",
2101
2164
  ...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
2102
2165
  });
2103
- const { planId } = requirePersistedPlan(plan, "starting deploy");
2104
- const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, opts.requiredPlan), "starting deploy");
2166
+ const { planId, operationId } = requirePersistedPlan(plan, "starting deploy");
2167
+ const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, opts.requiredPlan, undefined, operationId), "starting deploy");
2105
2168
  return await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project_id, sliceKinds);
2106
2169
  })();
2107
2170
  // Avoid an unhandled-rejection at construction time. Consumers must call
@@ -4034,7 +4097,7 @@ async function uploadInlineCas(client, projectId, bytes, contentType) {
4034
4097
  await uploadOne(client.fetch, session, bytes);
4035
4098
  // v1.48 unified-apply: per-session /storage/v1/uploads/:id/complete is
4036
4099
  // gone (404). The plan-level commit below promotes the session to CAS.
4037
- await client.request(`/content/v1/plans/${encodeURIComponent(planRes.plan_id)}/commit`, { method: "POST", headers, body: {}, context: "committing content upload" });
4100
+ await commitContentPlanWithRetry(client, planRes.plan_id, headers);
4038
4101
  }
4039
4102
  return { sha256, size: bytes.byteLength, contentType };
4040
4103
  }