@moinulmoin/eden 0.4.1 → 0.6.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.
@@ -11,6 +11,7 @@ import { EveCliError, } from "./eve.js";
11
11
  import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, } from "./eve-packaging.js";
12
12
  import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
13
13
  import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
14
+ import { createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
14
15
  const execFileAsync = promisify(execFile);
15
16
  const require = createRequire(import.meta.url);
16
17
  const wranglerEntrypoint = require.resolve("wrangler");
@@ -290,7 +291,7 @@ function humanEveResultLines(result, startedAt, eveVersion) {
290
291
  if (result.ok) {
291
292
  if (result.command === "eve deploy") {
292
293
  const url = result.deployment?.stableWorkersDevOrigin;
293
- lines.push("", `✓ deployed ${result.name} (${result.environment})` +
294
+ lines.push("", `✓ ${result.deployment?.operation === "update" ? "updated" : "deployed"} ${result.name} (${result.environment})` +
294
295
  (eveVersion === undefined ? "" : ` · eve ${eveVersion}`));
295
296
  if (url !== undefined)
296
297
  lines.push(url);
@@ -306,7 +307,7 @@ function humanEveResultLines(result, startedAt, eveVersion) {
306
307
  lines.push("", `✗ ${commandLabel} ${result.deployment.status}`, ` → ${result.deployment.status === "indeterminate"
307
308
  ? "The outcome is indeterminate; inspect the retained ownership evidence before retrying the same exact target."
308
309
  : result.deployment.status === "deployed"
309
- ? "The deployment is live; only local runtime-image cleanup failed, so do not retry the deployment."
310
+ ? "The deployment is live; only image cleanup failed (local runtime image or the superseded registry tag), so do not retry the deployment."
310
311
  : "The deployment failed before promotion; the exact target was not claimed."}`);
311
312
  }
312
313
  else if (firstFailure !== undefined) {
@@ -440,10 +441,13 @@ function exactContainerEntries(value, name) {
440
441
  typeof entry.name !== "string") {
441
442
  return undefined;
442
443
  }
443
- const id = entry.id;
444
444
  const entryName = entry.name;
445
- if (entryName === name)
446
- entries.push({ id, name: entryName });
445
+ if (entryName === name) {
446
+ entries.push({
447
+ id: entry.id,
448
+ name: entryName,
449
+ });
450
+ }
447
451
  }
448
452
  if (entries.length === 0 && collection.length >= 100) {
449
453
  return undefined;
@@ -595,6 +599,9 @@ async function defaultCloudflareRead(request) {
595
599
  target: { state: "absent" },
596
600
  };
597
601
  }
602
+ const workerPresent = deploymentEntries.length > 0;
603
+ const containerPresent = (containerEntries?.length ?? 0) > 0;
604
+ const providerEvidence = await readProviderEvidence(request.name, accountId, workerPresent, containerPresent ? containerEntries?.[0] : undefined);
598
605
  return {
599
606
  accountAccess,
600
607
  containerAccess,
@@ -605,16 +612,175 @@ async function defaultCloudflareRead(request) {
605
612
  ? {}
606
613
  : { workersDevSubdomain }),
607
614
  target: {
608
- state: "ambiguous",
609
- message: "The exact target could not be proven absent or owned from the read-only Cloudflare inventory.",
610
- remediation: "Resolve the exact Worker and Container ownership state before deployment; no target was claimed or changed.",
615
+ // The read-only inventory proves the exact target exists but cannot
616
+ // prove who created it; Eden's immutable deployment record resolves
617
+ // ownership next.
618
+ state: "unowned",
619
+ observed: { worker: workerPresent, container: containerPresent },
620
+ ...(providerEvidence === undefined ? {} : { providerEvidence }),
621
+ message: "The exact Worker or Container target already exists, and the read-only inventory cannot prove Eden owns it.",
622
+ remediation: "Deploy under a different --name or remove the conflicting Worker/Container yourself; Eden never adopts or replaces resources it cannot prove it created.",
623
+ },
624
+ };
625
+ }
626
+ /**
627
+ * Reads provider-backed facts about the live target: the Container
628
+ * application's identity from `wrangler containers list` plus `containers
629
+ * info` (scheduling policy and Durable Object namespace), and the
630
+ * `EDEN_EVE_DEPLOYMENT_ID` plain-text var plus the `EVE_CONTAINER` Durable
631
+ * Object namespace Eden stamps into every Worker it publishes (from the
632
+ * script settings API). All are provider state — a foreign Worker can forge
633
+ * `x-eden-*` response headers but cannot set vars or namespaces Eden
634
+ * recorded at publish time. The ownership proof is the Container
635
+ * application's Durable Object namespace matching the Worker's binding
636
+ * namespace.
637
+ */
638
+ async function readProviderEvidence(workerName, accountId, workerPresent, containerEntry) {
639
+ const evidence = {
640
+ ...(containerEntry === undefined
641
+ ? {}
642
+ : { containerApplicationId: containerEntry.id }),
643
+ };
644
+ if (containerEntry !== undefined) {
645
+ const info = await readWranglerJson([
646
+ "containers",
647
+ "info",
648
+ containerEntry.id,
649
+ "--json",
650
+ ]);
651
+ if (!info.failed && typeof info.value === "object" && info.value !== null) {
652
+ const infoRecord = info.value;
653
+ if (typeof infoRecord.scheduling_policy === "string") {
654
+ evidence.containerSchedulingPolicy = infoRecord.scheduling_policy;
655
+ }
656
+ const namespaceId = infoRecord.durable_objects?.namespace_id;
657
+ if (typeof namespaceId === "string") {
658
+ evidence.containerNamespaceId = namespaceId;
659
+ }
660
+ }
661
+ }
662
+ if (workerPresent && accountId !== undefined) {
663
+ const token = process.env.CLOUDFLARE_API_TOKEN ??
664
+ (await readWranglerOAuthToken());
665
+ if (token !== undefined) {
666
+ try {
667
+ const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${encodeURIComponent(workerName)}/settings`, {
668
+ headers: { authorization: `Bearer ${token}` },
669
+ signal: AbortSignal.timeout(20_000),
670
+ });
671
+ if (response.ok) {
672
+ evidence.settingsReadable = true;
673
+ const parsed = (await response.json());
674
+ const bindings = parsed.result?.bindings ?? [];
675
+ const deploymentBinding = bindings.find((value) => value.type === "plain_text" &&
676
+ value.name === "EDEN_EVE_DEPLOYMENT_ID");
677
+ if (typeof deploymentBinding?.text === "string") {
678
+ evidence.workerDeploymentId = deploymentBinding.text;
679
+ }
680
+ const containerBinding = bindings.find((value) => value.type === "durable_object_namespace" &&
681
+ value.name === "EVE_CONTAINER");
682
+ if (typeof containerBinding?.namespace_id === "string") {
683
+ evidence.workerContainerNamespaceId = containerBinding.namespace_id;
684
+ }
685
+ }
686
+ else if (response.status !== 404) {
687
+ evidence.settingsReadable = false;
688
+ }
689
+ }
690
+ catch {
691
+ evidence.settingsReadable = false;
692
+ }
693
+ }
694
+ }
695
+ return evidence;
696
+ }
697
+ /**
698
+ * Resolves `unowned` reads against Eden's immutable deployment records AND
699
+ * the live remote target. The local record proves Eden once created the
700
+ * target; the remote Worker's stamped identity proves it is still the one
701
+ * Eden deployed — a same-name Worker recreated by something else after the
702
+ * target was destroyed (or a forged local record) must never be upgraded to
703
+ * `owned`.
704
+ */
705
+ async function resolveEveTargetOwnership(request, result) {
706
+ if (result.target.state !== "unowned")
707
+ return result;
708
+ const record = await readEveDeploymentRecord(request.projectRoot, request.environment, request.name, result.accountId, request.projectId === "unknown-project" ? undefined : request.projectId);
709
+ if (record === undefined ||
710
+ record.identity.workerName !== request.name ||
711
+ record.identity.accountId !== result.accountId ||
712
+ (request.projectId !== "unknown-project" &&
713
+ record.identity.projectId !== undefined &&
714
+ record.identity.projectId !== request.projectId) ||
715
+ (result.target.observed?.container === true &&
716
+ record.identity.containerApplicationName !==
717
+ boundedResourceName(request.name, "container"))) {
718
+ return result;
719
+ }
720
+ // Remote proof must come from provider state. A foreign Worker can echo
721
+ // `x-eden-*` response headers, but it cannot plant the plain-text var Eden
722
+ // publishes into script settings or claim the Durable Object namespace the
723
+ // Worker's EVE_CONTAINER binding resolves to. The Worker is proven only by
724
+ // its own settings: unreadable settings fail closed.
725
+ const evidence = result.target.providerEvidence;
726
+ const recordedDeploymentIds = new Set(record.deploymentIds);
727
+ const workerProven = result.target.observed?.worker !== true ||
728
+ (evidence?.settingsReadable === true &&
729
+ evidence.workerDeploymentId !== undefined &&
730
+ recordedDeploymentIds.has(evidence.workerDeploymentId));
731
+ // The Container proof is the application running the durable_object
732
+ // scheduling policy with a namespace that matches the Worker binding's
733
+ // namespace; anything else fails closed.
734
+ const containerProven = result.target.observed?.container !== true ||
735
+ (evidence?.containerSchedulingPolicy === "durable_object" &&
736
+ evidence.containerNamespaceId !== undefined &&
737
+ evidence.workerContainerNamespaceId !== undefined &&
738
+ evidence.containerNamespaceId === evidence.workerContainerNamespaceId);
739
+ const remoteProven = evidence !== undefined && workerProven && containerProven;
740
+ if (!remoteProven) {
741
+ return {
742
+ ...result,
743
+ target: {
744
+ ...result.target,
745
+ message: "The exact Worker or Container target exists, and although a local Eden record matches, provider state cannot prove it is the one Eden deployed — the target may have been recreated outside Eden.",
746
+ remediation: "Inspect the Worker at its workers.dev origin and the recorded deployment history before redeploying; if it is not Eden's, deploy under a different --name or delete it yourself.",
747
+ },
748
+ };
749
+ }
750
+ const priorIdentity = {
751
+ workerName: record.identity.workerName,
752
+ containerApplicationName: record.identity.containerApplicationName,
753
+ stableContainerInstanceName: record.identity.stableContainerInstanceName,
754
+ accountId: record.identity.accountId,
755
+ environment: request.environment,
756
+ name: request.name,
757
+ ...(record.identity.projectId === undefined
758
+ ? {}
759
+ : { projectId: record.identity.projectId }),
760
+ ...(record.identity.generationId === undefined
761
+ ? {}
762
+ : { generationId: record.identity.generationId }),
763
+ ...(record.identity.containerImage === undefined
764
+ ? {}
765
+ : { containerImage: record.identity.containerImage }),
766
+ };
767
+ return {
768
+ ...result,
769
+ target: {
770
+ state: "owned",
771
+ identity: priorIdentity,
772
+ ...(result.target.observed === undefined
773
+ ? {}
774
+ : { observed: result.target.observed }),
775
+ message: "The immutable deployment record proves Eden owns the exact target; deploy proceeds as an in-place update.",
611
776
  },
612
777
  };
613
778
  }
614
779
  async function readEveCloudflareTarget(request, options) {
615
- return options.cloudflareRead === undefined
616
- ? defaultCloudflareRead(request)
617
- : options.cloudflareRead(request);
780
+ const result = options.cloudflareRead === undefined
781
+ ? await defaultCloudflareRead(request)
782
+ : await options.cloudflareRead(request);
783
+ return resolveEveTargetOwnership(request, result);
618
784
  }
619
785
  async function resolvedCloudflareOrigin(result, workerName) {
620
786
  if (result.accountId === undefined ||
@@ -622,7 +788,6 @@ async function resolvedCloudflareOrigin(result, workerName) {
622
788
  return undefined;
623
789
  }
624
790
  try {
625
- const { resolveStableWorkersDevOrigin } = await import("@moinulmoin/eden-runtime-cloudflare");
626
791
  const origin = resolveStableWorkersDevOrigin({
627
792
  workerName,
628
793
  workersDevSubdomain: result.workersDevSubdomain,
@@ -713,12 +878,12 @@ function cloudflareChecks(result) {
713
878
  ? undefined
714
879
  : "Use an authenticated read-only adapter with account and Container access; preflight never uploads, publishes, or changes remote state.";
715
880
  const target = result.target;
716
- const targetPasses = target.state === "absent" ||
717
- (target.state === "owned" && target.matchesCandidate === true);
881
+ const targetPasses = target.state === "absent" || target.state === "owned";
718
882
  const targetMessage = targetPasses
719
883
  ? target.state === "absent"
720
884
  ? "The exact target read is absent and is eligible without claiming ownership."
721
- : "The exact target is already owned by this candidate identity; no ownership claim was attempted."
885
+ : target.message ??
886
+ "The exact target is provably Eden-owned; deploy proceeds as an in-place update."
722
887
  : target.message ??
723
888
  "The exact target read is unowned, mismatched, cross-environment, or ambiguous.";
724
889
  const targetRemediation = targetPasses
@@ -989,8 +1154,6 @@ function defaultPublicationRunner() {
989
1154
  "--env",
990
1155
  request.identity.environment,
991
1156
  "--strict",
992
- "--containers-rollout",
993
- "immediate",
994
1157
  ], dirname(request.wranglerConfigPath));
995
1158
  if (result.exitCode !== 0) {
996
1159
  return {
@@ -1062,6 +1225,10 @@ function responseIdentity(response, body) {
1062
1225
  read("workerName", "x-eden-eve-worker-name");
1063
1226
  read("containerApplicationName", "x-eden-eve-container-application");
1064
1227
  read("stableContainerInstanceName", "x-eden-eve-container-instance");
1228
+ const started = response.headers.get("x-eden-eve-started-deployment");
1229
+ if (started !== null) {
1230
+ identity["startedDeploymentId"] = started;
1231
+ }
1065
1232
  read("containerImage", "x-eden-eve-container-image");
1066
1233
  return identity;
1067
1234
  }
@@ -1105,9 +1272,20 @@ function defaultHealthRunner() {
1105
1272
  body.state === "ready" ||
1106
1273
  body.ready === true);
1107
1274
  if (ready) {
1275
+ const observedIdentity = responseIdentity(response, body);
1276
+ // A Worker activates before its image rollout completes; promotion
1277
+ // must wait until the *served container* verifiably runs this
1278
+ // generation's image (the host stamps the header only after
1279
+ // inspecting the running instance).
1280
+ if (observedIdentity.startedDeploymentId !== request.identity.deploymentId) {
1281
+ lastReason =
1282
+ "The served Container has not verifiably started this deployment's image yet; waiting for the rollout to land.";
1283
+ await wait();
1284
+ continue;
1285
+ }
1108
1286
  return {
1109
1287
  status: "ready",
1110
- identity: responseIdentity(response, body),
1288
+ identity: observedIdentity,
1111
1289
  };
1112
1290
  }
1113
1291
  lastReason =
@@ -1161,8 +1339,13 @@ async function releaseEveTargetLock(lock) {
1161
1339
  await rm(lock.path, { force: true });
1162
1340
  return (await lstat(lock.path).catch(() => undefined)) === undefined;
1163
1341
  }
1164
- async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot) {
1165
- const targetRoot = join(deploymentRoot, "targets", targetKey);
1342
+ /**
1343
+ * Atomically re-points the target's CURRENT symlink at a promoted immutable
1344
+ * generation. The pointer key is `<environment>-<name>` — the same path
1345
+ * `destroy` clears and the update ownership resolver reads.
1346
+ */
1347
+ async function promoteEveTargetPointer(deploymentRoot, pointerKey, generationRoot) {
1348
+ const targetRoot = join(deploymentRoot, "targets", pointerKey);
1166
1349
  const artifactBoundary = await realpath(dirname(deploymentRoot)).catch(() => undefined);
1167
1350
  const generationCanonical = await realpath(generationRoot).catch(() => undefined);
1168
1351
  if (artifactBoundary === undefined ||
@@ -1191,7 +1374,7 @@ async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot
1191
1374
  throw new EveCliError({
1192
1375
  code: "EVE_DEPLOY_PROMOTION_FAILED",
1193
1376
  message: "The Eve target pointer directory could not be verified safely.",
1194
- source: targetKey,
1377
+ source: pointerKey,
1195
1378
  });
1196
1379
  }
1197
1380
  const currentPath = join(targetRoot, "CURRENT");
@@ -1208,7 +1391,7 @@ async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot
1208
1391
  throw new EveCliError({
1209
1392
  code: "EVE_DEPLOY_PROMOTION_FAILED",
1210
1393
  message: "The Eve target pointer did not resolve to the promoted generation.",
1211
- source: targetKey,
1394
+ source: pointerKey,
1212
1395
  });
1213
1396
  }
1214
1397
  return currentPath;
@@ -1242,9 +1425,10 @@ async function writeEveIndeterminateEvidence(candidate, evidence, extra = {}) {
1242
1425
  flag: "wx",
1243
1426
  });
1244
1427
  }
1245
- function deploymentMetadata(identity, targetKey, status) {
1428
+ function deploymentMetadata(identity, targetKey, status, operation, superseded) {
1246
1429
  return {
1247
1430
  status,
1431
+ operation,
1248
1432
  targetKey,
1249
1433
  deploymentId: identity.deploymentId,
1250
1434
  workerName: identity.workerName,
@@ -1252,6 +1436,12 @@ function deploymentMetadata(identity, targetKey, status) {
1252
1436
  stableContainerInstanceName: identity.stableContainerInstanceName,
1253
1437
  stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
1254
1438
  imageReference: identity.containerImage,
1439
+ ...(superseded?.generationId === undefined
1440
+ ? {}
1441
+ : { supersededGenerationId: superseded.generationId }),
1442
+ ...(superseded?.containerImage === undefined
1443
+ ? {}
1444
+ : { supersededImageReference: superseded.containerImage }),
1255
1445
  runtimeVariableNames: identity.runtimeVariableNames,
1256
1446
  evidenceRetained: true,
1257
1447
  };
@@ -1273,6 +1463,46 @@ async function runEveDeployment(request, options, collected) {
1273
1463
  collected.projectId === undefined) {
1274
1464
  throw deploymentFailure("EVE_DEPLOY_CHECKS_FAILED", "The inline Eve deploy checks did not return a complete immutable candidate.");
1275
1465
  }
1466
+ const isUpdate = cloudflare.target.state === "owned";
1467
+ const operation = isUpdate ? "update" : "create";
1468
+ const prior = collected.priorDeployment;
1469
+ if (isUpdate && prior === undefined) {
1470
+ throw deploymentFailure("EVE_UPDATE_RECORD_UNAVAILABLE", "The exact target is provably Eden-owned, but its immutable deployment record is unreadable; refusing to update without the recorded Durable Object history.");
1471
+ }
1472
+ if (isUpdate && prior?.durableObjects === undefined) {
1473
+ throw deploymentFailure("EVE_UPDATE_RECORD_UNAVAILABLE", "The prior deployment record does not carry a readable Durable Object history; refusing to update because replacing it could emit a divergent migration list.");
1474
+ }
1475
+ const priorIdentity = prior?.identity;
1476
+ const priorDurableObjects = prior?.durableObjects;
1477
+ const superseded = isUpdate
1478
+ ? {
1479
+ generationId: priorIdentity?.generationId,
1480
+ containerImage: priorIdentity?.containerImage,
1481
+ }
1482
+ : undefined;
1483
+ const resultWarnings = collected.result.warnings;
1484
+ if (isUpdate) {
1485
+ const world = collected.workflowWorld;
1486
+ const durableWorld = world !== undefined &&
1487
+ world !== "local" &&
1488
+ world !== "@workflow/world-local";
1489
+ if (durableWorld) {
1490
+ resultWarnings.push({
1491
+ id: "EVE_UPDATE_IN_FLIGHT",
1492
+ message: "This update replaces the running Container; durable World runs in flight resume on the new code — Cloudflare has no deployment pinning, so keep step names and payload shapes backward compatible.",
1493
+ remediation: "Let pending approvals and long runs settle before updating, or keep step names, queue names, and serialized payloads compatible across the change.",
1494
+ });
1495
+ }
1496
+ if (prior?.worldCloudflare !==
1497
+ (world === "@moinulmoin/eden-world-cloudflare")) {
1498
+ resultWarnings.push({
1499
+ id: "EVE_UPDATE_WORLD_SWITCH",
1500
+ message: "The compiled Workflow World changed since the last deploy; existing sessions and pending approvals stay in the previous World and do not migrate to the new one.",
1501
+ remediation: "Existing Durable Object data is preserved (classes and bindings are never removed), but runs started before this update remain where they were created.",
1502
+ });
1503
+ }
1504
+ eveProgress(options, "update: Durable Object data is preserved; the running Container restarts on its next request to serve the new image");
1505
+ }
1276
1506
  const accountId = cloudflare.accountId ??
1277
1507
  cloudflare.target.identity?.accountId;
1278
1508
  const workersDevSubdomain = cloudflare.workersDevSubdomain ??
@@ -1282,7 +1512,6 @@ async function runEveDeployment(request, options, collected) {
1282
1512
  }
1283
1513
  let stableWorkersDevOrigin;
1284
1514
  try {
1285
- const { resolveStableWorkersDevOrigin } = await import("@moinulmoin/eden-runtime-cloudflare");
1286
1515
  stableWorkersDevOrigin = resolveStableWorkersDevOrigin({
1287
1516
  workerName: request.name,
1288
1517
  workersDevSubdomain,
@@ -1399,31 +1628,39 @@ async function runEveDeployment(request, options, collected) {
1399
1628
  ? {}
1400
1629
  : { runtimeRevisionHandle: runtimeInjection.protectedHandle }),
1401
1630
  };
1631
+ const baseConfigRequest = {
1632
+ accountId,
1633
+ workerName: identity.workerName,
1634
+ containerApplicationName: identity.containerApplicationName,
1635
+ containerClassName: "EveHostDurableContainer",
1636
+ containerBindingName: "EVE_CONTAINER",
1637
+ stableContainerInstanceName: identity.stableContainerInstanceName,
1638
+ deploymentId: identity.deploymentId,
1639
+ generationId: identity.generationId,
1640
+ stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
1641
+ containerImage: identity.containerImage,
1642
+ runtimeVariableNames: identity.runtimeVariableNames,
1643
+ ...(identity.runtimeRevisionHandle === undefined
1644
+ ? {}
1645
+ : { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
1646
+ schedules: collected.schedules ?? [],
1647
+ ...(collected.workflowWorld === undefined
1648
+ ? {}
1649
+ : { workflowWorld: collected.workflowWorld }),
1650
+ // In-place updates carry the prior Durable Object bindings and
1651
+ // migration history forward verbatim: classes are append-only and are
1652
+ // never silently re-declared or removed.
1653
+ ...(isUpdate && priorDurableObjects !== undefined
1654
+ ? { durableObjects: priorDurableObjects }
1655
+ : {}),
1656
+ ...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
1657
+ ? {}
1658
+ : { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
1659
+ };
1402
1660
  let hostConfig;
1403
1661
  try {
1404
- const { createEveHostConfig, } = await import("@moinulmoin/eden-runtime-cloudflare");
1405
1662
  hostConfig = createEveHostConfig({
1406
- accountId,
1407
- workerName: identity.workerName,
1408
- containerApplicationName: identity.containerApplicationName,
1409
- containerClassName: "EveHostContainer",
1410
- containerBindingName: "EVE_CONTAINER",
1411
- stableContainerInstanceName: identity.stableContainerInstanceName,
1412
- deploymentId: identity.deploymentId,
1413
- generationId: identity.generationId,
1414
- stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
1415
- containerImage: identity.containerImage,
1416
- runtimeVariableNames: identity.runtimeVariableNames,
1417
- ...(identity.runtimeRevisionHandle === undefined
1418
- ? {}
1419
- : { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
1420
- schedules: collected.schedules ?? [],
1421
- ...(collected.workflowWorld === undefined
1422
- ? {}
1423
- : { workflowWorld: collected.workflowWorld }),
1424
- ...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
1425
- ? {}
1426
- : { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
1663
+ ...baseConfigRequest,
1427
1664
  });
1428
1665
  }
1429
1666
  catch (error) {
@@ -1431,7 +1668,6 @@ async function runEveDeployment(request, options, collected) {
1431
1668
  ? error.message
1432
1669
  : "The exact Eve Worker/Container host configuration was invalid.");
1433
1670
  }
1434
- const { generateEveHostWorkerSource } = await import("@moinulmoin/eden-runtime-cloudflare");
1435
1671
  const workerSource = generateEveHostWorkerSource({ config: hostConfig });
1436
1672
  const paths = await writeEveDeploymentArtifacts(candidate, hostConfig, workerSource);
1437
1673
  const publicationRequest = {
@@ -1441,14 +1677,27 @@ async function runEveDeployment(request, options, collected) {
1441
1677
  ...paths,
1442
1678
  ...(runtimeInjection === undefined ? {} : { runtimeInjection }),
1443
1679
  };
1680
+ /** Publishes one wrangler config; returns its terminal result. */
1681
+ const publishOne = async (request) => (options.publish ?? defaultPublicationRunner())(request);
1444
1682
  let publication;
1683
+ const writeFailureRecord = async (status, evidence) => {
1684
+ try {
1685
+ await writeEveDeploymentRecord(candidate, identity, hostConfig, status, {
1686
+ evidence,
1687
+ });
1688
+ }
1689
+ catch (error) {
1690
+ if (error.code !== "EEXIST")
1691
+ throw error;
1692
+ }
1693
+ };
1445
1694
  try {
1446
1695
  eveProgress(options, "publishing Worker and Container application");
1447
- publication = await (options.publish ?? defaultPublicationRunner())(publicationRequest);
1696
+ publication = await publishOne(publicationRequest);
1448
1697
  }
1449
1698
  catch {
1450
- await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: "publication-threw-before-terminal-result" });
1451
- const deployment = deploymentMetadata(identity, targetKey, "indeterminate");
1699
+ await writeFailureRecord("indeterminate", "publication-threw-before-terminal-result");
1700
+ const deployment = deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded);
1452
1701
  return {
1453
1702
  ...collected.result,
1454
1703
  ok: false,
@@ -1456,11 +1705,11 @@ async function runEveDeployment(request, options, collected) {
1456
1705
  };
1457
1706
  }
1458
1707
  if (publication.status === "indeterminate") {
1459
- await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: safeText(publication.reason) });
1708
+ await writeFailureRecord("indeterminate", safeText(publication.reason));
1460
1709
  return {
1461
1710
  ...collected.result,
1462
1711
  ok: false,
1463
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1712
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1464
1713
  };
1465
1714
  }
1466
1715
  if (publication.status === "failed" ||
@@ -1472,7 +1721,7 @@ async function runEveDeployment(request, options, collected) {
1472
1721
  return {
1473
1722
  ...collected.result,
1474
1723
  ok: false,
1475
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1724
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1476
1725
  };
1477
1726
  }
1478
1727
  if (publication.status === "failed" &&
@@ -1483,7 +1732,7 @@ async function runEveDeployment(request, options, collected) {
1483
1732
  return {
1484
1733
  ...collected.result,
1485
1734
  ok: false,
1486
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1735
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1487
1736
  };
1488
1737
  }
1489
1738
  if (publication.status === "failed" &&
@@ -1501,7 +1750,7 @@ async function runEveDeployment(request, options, collected) {
1501
1750
  return {
1502
1751
  ...collected.result,
1503
1752
  ok: false,
1504
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1753
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1505
1754
  };
1506
1755
  }
1507
1756
  }
@@ -1513,7 +1762,7 @@ async function runEveDeployment(request, options, collected) {
1513
1762
  return {
1514
1763
  ...collected.result,
1515
1764
  ok: false,
1516
- deployment: deploymentMetadata(identity, targetKey, "failed"),
1765
+ deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
1517
1766
  };
1518
1767
  }
1519
1768
  let health;
@@ -1529,7 +1778,7 @@ async function runEveDeployment(request, options, collected) {
1529
1778
  return {
1530
1779
  ...collected.result,
1531
1780
  ok: false,
1532
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1781
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1533
1782
  };
1534
1783
  }
1535
1784
  if (health.status === "indeterminate") {
@@ -1537,7 +1786,7 @@ async function runEveDeployment(request, options, collected) {
1537
1786
  return {
1538
1787
  ...collected.result,
1539
1788
  ok: false,
1540
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1789
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1541
1790
  };
1542
1791
  }
1543
1792
  const observedHealthIdentity = health.status === "ready"
@@ -1554,7 +1803,7 @@ async function runEveDeployment(request, options, collected) {
1554
1803
  return {
1555
1804
  ...collected.result,
1556
1805
  ok: false,
1557
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1806
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1558
1807
  };
1559
1808
  }
1560
1809
  if (publication.createdByAttempt === true &&
@@ -1564,7 +1813,7 @@ async function runEveDeployment(request, options, collected) {
1564
1813
  return {
1565
1814
  ...collected.result,
1566
1815
  ok: false,
1567
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1816
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1568
1817
  };
1569
1818
  }
1570
1819
  if (publication.createdByAttempt === true &&
@@ -1578,7 +1827,7 @@ async function runEveDeployment(request, options, collected) {
1578
1827
  return {
1579
1828
  ...collected.result,
1580
1829
  ok: false,
1581
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1830
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1582
1831
  };
1583
1832
  }
1584
1833
  }
@@ -1590,7 +1839,7 @@ async function runEveDeployment(request, options, collected) {
1590
1839
  return {
1591
1840
  ...collected.result,
1592
1841
  ok: false,
1593
- deployment: deploymentMetadata(identity, targetKey, "failed"),
1842
+ deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
1594
1843
  };
1595
1844
  }
1596
1845
  try {
@@ -1609,7 +1858,7 @@ async function runEveDeployment(request, options, collected) {
1609
1858
  return {
1610
1859
  ...collected.result,
1611
1860
  ok: false,
1612
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1861
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1613
1862
  };
1614
1863
  }
1615
1864
  await writeEveDeploymentRecord(candidate, identity, hostConfig, "failed", {
@@ -1620,7 +1869,7 @@ async function runEveDeployment(request, options, collected) {
1620
1869
  return {
1621
1870
  ...collected.result,
1622
1871
  ok: false,
1623
- deployment: deploymentMetadata(identity, targetKey, "failed"),
1872
+ deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
1624
1873
  };
1625
1874
  }
1626
1875
  await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", {
@@ -1631,7 +1880,7 @@ async function runEveDeployment(request, options, collected) {
1631
1880
  return {
1632
1881
  ...collected.result,
1633
1882
  ok: false,
1634
- deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
1883
+ deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
1635
1884
  };
1636
1885
  }
1637
1886
  await writeEveDeploymentRecord(candidate, identity, hostConfig, "deployed", {
@@ -1641,8 +1890,34 @@ async function runEveDeployment(request, options, collected) {
1641
1890
  identity: "exact",
1642
1891
  },
1643
1892
  });
1644
- await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), targetKey, candidate.generationRoot);
1893
+ await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), `${request.environment}-${request.name}`, candidate.generationRoot);
1645
1894
  await options.afterPromotion?.();
1895
+ let supersededImageCleanup;
1896
+ if (isUpdate) {
1897
+ // The superseded generation's registry repository is generation-scoped
1898
+ // and no longer referenced once CURRENT promotes this generation; only
1899
+ // then is it safe to delete. Every tag in the repository goes with it —
1900
+ // including the snapshot tags Cloudflare created under it.
1901
+ const previousRepository = priorIdentity === undefined
1902
+ ? undefined
1903
+ : ownedRegistryRepository(priorIdentity, request.environment, request.name, accountId);
1904
+ if (previousRepository !== undefined) {
1905
+ eveProgress(options, `deleting superseded registry repository ${previousRepository}`);
1906
+ let deleted = "indeterminate";
1907
+ try {
1908
+ deleted = await (options.deleteRegistryRepository ??
1909
+ defaultRegistryRepositoryDelete)({ repository: previousRepository });
1910
+ }
1911
+ catch {
1912
+ deleted = "indeterminate";
1913
+ }
1914
+ supersededImageCleanup = deleted === "indeterminate"
1915
+ ? check("VAL-LIFE-007", "failed", `The update is healthy and promoted, but the superseded registry repository ${previousRepository} could not be proven deleted.`, `Remove only the recorded repository's tags ${previousRepository}; do not retry or run broad registry cleanup.`)
1916
+ : check("VAL-LIFE-007", "passed", deleted === "deleted"
1917
+ ? "The superseded generation's registry repository (all tags, including snapshots) was deleted after promotion."
1918
+ : "The superseded generation's registry repository was already absent.");
1919
+ }
1920
+ }
1646
1921
  let runtimeImageCleanup;
1647
1922
  if (runtimeEvidence.cleanup.imageRetained) {
1648
1923
  if (options.retainRuntimeImage === true) {
@@ -1666,13 +1941,18 @@ async function runEveDeployment(request, options, collected) {
1666
1941
  : check("VAL-CROSS-004", "failed", "The remote deployment is healthy, but exact local runtime-image cleanup could not be verified.", `Remove only the recorded image ${imageDigest}; do not retry or run broad Docker cleanup.`);
1667
1942
  }
1668
1943
  }
1944
+ const postPromotionChecks = [
1945
+ ...(supersededImageCleanup === undefined ? [] : [supersededImageCleanup]),
1946
+ ...(runtimeImageCleanup === undefined ? [] : [runtimeImageCleanup]),
1947
+ ];
1669
1948
  return {
1670
1949
  ...collected.result,
1671
- ok: runtimeImageCleanup?.status !== "failed",
1672
- ...(runtimeImageCleanup === undefined
1950
+ ok: runtimeImageCleanup?.status !== "failed" &&
1951
+ supersededImageCleanup?.status !== "failed",
1952
+ ...(postPromotionChecks.length === 0
1673
1953
  ? {}
1674
- : { checks: [...collected.result.checks, runtimeImageCleanup] }),
1675
- deployment: deploymentMetadata(identity, targetKey, "deployed"),
1954
+ : { checks: [...collected.result.checks, ...postPromotionChecks] }),
1955
+ deployment: deploymentMetadata(identity, targetKey, "deployed", operation, superseded),
1676
1956
  };
1677
1957
  }
1678
1958
  async function collectEvePreflight(request, options) {
@@ -1813,9 +2093,6 @@ async function collectEvePreflight(request, options) {
1813
2093
  }
1814
2094
  warnings.push(...(await collectEveWorldWarnings(candidate, undefined)));
1815
2095
  const schedules = await readCompiledSchedules(candidate);
1816
- // The runtime package is imported lazily so `eden` commands that never
1817
- // reach deploy checks stay fast; the module specifier is fixed.
1818
- const { parseEveScheduleCron } = await import("@moinulmoin/eden-runtime-cloudflare");
1819
2096
  const expressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) !== undefined);
1820
2097
  const unexpressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) === undefined);
1821
2098
  if (unexpressible.length > 0) {
@@ -1848,12 +2125,14 @@ async function collectEvePreflight(request, options) {
1848
2125
  }
1849
2126
  let cloudflare;
1850
2127
  let publicOrigin;
2128
+ let priorDeployment;
1851
2129
  if (request.command === "deploy") {
1852
2130
  eveProgress(options, "checking Cloudflare access and exact target");
1853
2131
  try {
1854
2132
  cloudflare = await readEveCloudflareTarget({
1855
2133
  projectId: packaging.project?.projectId ?? "unknown-project",
1856
2134
  sourceDigest: candidate.sourceDigest,
2135
+ projectRoot: request.projectRoot,
1857
2136
  environment: request.environment,
1858
2137
  name: request.name,
1859
2138
  }, options);
@@ -1875,6 +2154,17 @@ async function collectEvePreflight(request, options) {
1875
2154
  };
1876
2155
  }
1877
2156
  checks.push(...cloudflareChecks(cloudflare));
2157
+ // An owned target also requires its immutable record so the update can
2158
+ // carry the published Durable Object history forward append-only.
2159
+ if (cloudflare.target.state === "owned") {
2160
+ const record = await readEveDeploymentRecord(request.projectRoot, request.environment, request.name, cloudflare.accountId, packaging.project?.projectId === "unknown-project"
2161
+ ? undefined
2162
+ : packaging.project?.projectId);
2163
+ priorDeployment = record ?? undefined;
2164
+ if (record === undefined || record.durableObjects === undefined) {
2165
+ checks.push(check("VAL-CLI-007-TARGET-CONFLICT", "failed", "The exact target is Eden-owned, but its immutable deployment record does not carry a readable Durable Object history.", "Re-run the last successful deploy generation or destroy the exact target; updating without the recorded migration history could diverge Durable Object declarations."));
2166
+ }
2167
+ }
1878
2168
  publicOrigin = await resolvedCloudflareOrigin(cloudflare, request.name);
1879
2169
  if (publicOrigin === undefined) {
1880
2170
  checks.push(check("VAL-HOST-006", "failed", "The authenticated Cloudflare read did not prove the exact workers.dev origin before Eve initialization.", "Return one authenticated account ID, one workers.dev subdomain, and the exact Worker-origin binding."));
@@ -2006,6 +2296,7 @@ async function collectEvePreflight(request, options) {
2006
2296
  cloudflare = await readEveCloudflareTarget({
2007
2297
  projectId: packaging.project?.projectId ?? "unknown-project",
2008
2298
  sourceDigest: candidate.sourceDigest,
2299
+ projectRoot: request.projectRoot,
2009
2300
  environment: request.environment,
2010
2301
  name: request.name,
2011
2302
  }, options);
@@ -2049,6 +2340,7 @@ async function collectEvePreflight(request, options) {
2049
2340
  // reported as EVE_SCHEDULE_UNSUPPORTED and fire only while awake.
2050
2341
  schedules: expressible,
2051
2342
  cloudflare,
2343
+ ...(priorDeployment === undefined ? {} : { priorDeployment }),
2052
2344
  ...(workflowWorld === undefined ? {} : { workflowWorld }),
2053
2345
  };
2054
2346
  }
@@ -2095,7 +2387,7 @@ export async function runEveControlPlane(request, options = {}) {
2095
2387
  message: status === "indeterminate"
2096
2388
  ? "The Eve deployment outcome is indeterminate; inspect the retained exact ownership evidence before retrying."
2097
2389
  : status === "deployed"
2098
- ? "The Eve deployment is healthy and promoted, but exact local runtime-image cleanup failed; do not retry the deployment."
2390
+ ? "The Eve deployment is healthy and promoted, but exact image cleanup failed; do not retry the deployment."
2099
2391
  : "The Eve deployment failed before exact target promotion.",
2100
2392
  });
2101
2393
  }
@@ -2223,6 +2515,161 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
2223
2515
  identity: verified,
2224
2516
  };
2225
2517
  }
2518
+ /**
2519
+ * Reads the immutable records for this target. The live identity comes from
2520
+ * CURRENT first, then the newest `deployed` record, then the newest record
2521
+ * of any status; the Durable Object history comes from the newest record
2522
+ * that carries one, healthy or not, so a failed update's published tags are
2523
+ * never dropped.
2524
+ */
2525
+ async function readEveDeploymentRecord(projectRoot, environment, name, accountId, projectId) {
2526
+ const targetsRoot = join(projectRoot, ".eden", "eve-deploy", "targets", `${environment}-${name}`);
2527
+ const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
2528
+ const entries = await readdir(generationsRoot).catch(() => []);
2529
+ const scanned = [];
2530
+ const deploymentIds = new Set();
2531
+ for (const entry of entries) {
2532
+ const generationRoot = join(generationsRoot, entry);
2533
+ const recordPath = join(generationRoot, "deployment.json");
2534
+ const details = await lstat(recordPath).catch(() => undefined);
2535
+ if (details === undefined || !details.isFile())
2536
+ continue;
2537
+ const parsed = await parseEveDeploymentRecord(recordPath, environment, name);
2538
+ if (parsed !== undefined) {
2539
+ // Only records for this account and (when known) this project may
2540
+ // arbitrate ownership or supply migration history.
2541
+ if ((accountId !== undefined && parsed.identity.accountId !== accountId) ||
2542
+ (projectId !== undefined && parsed.identity.projectId !== projectId)) {
2543
+ continue;
2544
+ }
2545
+ scanned.push({
2546
+ record: parsed,
2547
+ generationRoot,
2548
+ modifiedAt: details.mtimeMs,
2549
+ });
2550
+ const deploymentId = parsed.deploymentId;
2551
+ if (deploymentId !== undefined)
2552
+ deploymentIds.add(deploymentId);
2553
+ }
2554
+ }
2555
+ scanned.sort((left, right) => right.modifiedAt - left.modifiedAt);
2556
+ const current = await lstat(join(targetsRoot, "CURRENT")).catch(() => undefined);
2557
+ let live = scanned.find((entry) => entry.record.status === "deployed") ?? scanned[0];
2558
+ if (current !== undefined && current.isSymbolicLink()) {
2559
+ const generationRoot = await realpath(join(targetsRoot, "CURRENT")).catch(() => undefined);
2560
+ if (generationRoot !== undefined) {
2561
+ const pinned = scanned.find((entry) => entry.generationRoot === generationRoot);
2562
+ if (pinned !== undefined) {
2563
+ live = pinned;
2564
+ }
2565
+ else {
2566
+ const parsed = await parseEveDeploymentRecord(join(generationRoot, "deployment.json"), environment, name);
2567
+ if (parsed !== undefined &&
2568
+ !((accountId !== undefined &&
2569
+ parsed.identity.accountId !== accountId) ||
2570
+ (projectId !== undefined &&
2571
+ parsed.identity.projectId !== projectId))) {
2572
+ live = { record: parsed, generationRoot, modifiedAt: 0 };
2573
+ }
2574
+ }
2575
+ }
2576
+ }
2577
+ if (live === undefined)
2578
+ return undefined;
2579
+ // Newest record carrying a usable DO history wins, regardless of its
2580
+ // deployment outcome: a tag it published must never be replayed.
2581
+ const historySource = scanned.find((entry) => entry.record.durableObjects !== undefined);
2582
+ return {
2583
+ recordPath: live.record.recordPath,
2584
+ generationRoot: live.generationRoot,
2585
+ identity: live.record.identity,
2586
+ status: live.record.status,
2587
+ deploymentIds: [...deploymentIds],
2588
+ worldCloudflare: live.record.worldCloudflare,
2589
+ durableObjects: historySource?.record.durableObjects,
2590
+ };
2591
+ }
2592
+ async function parseEveDeploymentRecord(recordPath, environment, name) {
2593
+ const base = await parseEveDestroyRecord(recordPath, environment, name);
2594
+ if (base === undefined)
2595
+ return undefined;
2596
+ let parsed;
2597
+ try {
2598
+ parsed = JSON.parse(await readFile(recordPath, "utf8"));
2599
+ }
2600
+ catch {
2601
+ return undefined;
2602
+ }
2603
+ if (typeof parsed !== "object" || parsed === null)
2604
+ return undefined;
2605
+ const record = parsed;
2606
+ const identityRaw = parsed.identity;
2607
+ const deploymentId = typeof identityRaw === "object" &&
2608
+ identityRaw !== null &&
2609
+ typeof identityRaw
2610
+ .deploymentId === "string"
2611
+ ? identityRaw.deploymentId
2612
+ : undefined;
2613
+ const status = typeof record.status === "string" &&
2614
+ ["deployed", "failed", "indeterminate"].includes(record.status)
2615
+ ? record.status
2616
+ : undefined;
2617
+ const worker = typeof record.worker === "object" && record.worker !== null
2618
+ ? record.worker
2619
+ : undefined;
2620
+ const rawBindings = worker !== undefined && "durable_objects" in worker &&
2621
+ typeof worker.durable_objects === "object" &&
2622
+ worker.durable_objects !== null &&
2623
+ "bindings" in worker.durable_objects
2624
+ ? worker.durable_objects.bindings
2625
+ : undefined;
2626
+ const rawMigrations = worker !== undefined && "migrations" in worker
2627
+ ? worker.migrations
2628
+ : undefined;
2629
+ const rawVars = worker !== undefined && "vars" in worker &&
2630
+ typeof worker.vars === "object" && worker.vars !== null
2631
+ ? worker.vars
2632
+ : undefined;
2633
+ const bindings = [];
2634
+ const migrations = [];
2635
+ const durableObjectsValid = Array.isArray(rawBindings) &&
2636
+ Array.isArray(rawMigrations);
2637
+ const invalidHistory = () => ({
2638
+ recordPath,
2639
+ identity: base.identity,
2640
+ status,
2641
+ deploymentId,
2642
+ worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
2643
+ durableObjects: undefined,
2644
+ });
2645
+ if (durableObjectsValid) {
2646
+ for (const binding of rawBindings) {
2647
+ if (typeof binding !== "object" || binding === null ||
2648
+ typeof binding.name !== "string" ||
2649
+ typeof binding.class_name !== "string") {
2650
+ return invalidHistory();
2651
+ }
2652
+ bindings.push(binding);
2653
+ }
2654
+ for (const migration of rawMigrations) {
2655
+ if (typeof migration !== "object" || migration === null ||
2656
+ typeof migration.tag !== "string" ||
2657
+ !Array.isArray(migration.new_sqlite_classes) ||
2658
+ (migration.new_sqlite_classes ?? []).some((className) => typeof className !== "string")) {
2659
+ return invalidHistory();
2660
+ }
2661
+ migrations.push(migration);
2662
+ }
2663
+ }
2664
+ return {
2665
+ recordPath,
2666
+ identity: base.identity,
2667
+ status,
2668
+ deploymentId,
2669
+ worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
2670
+ durableObjects: durableObjectsValid ? { bindings, migrations } : undefined,
2671
+ };
2672
+ }
2226
2673
  async function defaultDestroyTargetRead(request) {
2227
2674
  const account = await readWranglerJson(["whoami", "--json"]);
2228
2675
  const deployments = await readWranglerJson([
@@ -2244,18 +2691,25 @@ async function defaultDestroyTargetRead(request) {
2244
2691
  (exactDeploymentEntries(deployments.value)?.length ?? 0) > 0;
2245
2692
  const entries = exactContainerEntries(containers.value, request.containerApplicationName);
2246
2693
  const firstContainer = entries === undefined ? undefined : entries[0];
2247
- const ownedImages = request.registryImages;
2248
- let registryImagesPresent;
2249
- if (ownedImages !== undefined && ownedImages.length > 0) {
2694
+ const ownedRepositories = request.registryRepositories;
2695
+ let registryTagsPresent;
2696
+ if (ownedRepositories !== undefined && ownedRepositories.length > 0) {
2250
2697
  const images = await readWranglerJson([
2251
2698
  "containers",
2252
2699
  "images",
2253
2700
  "list",
2254
2701
  "--json",
2255
2702
  ]);
2256
- const present = registryImageNames(images.value);
2257
- if (!images.failed && present !== undefined) {
2258
- registryImagesPresent = ownedImages.filter((image) => present.has(image));
2703
+ const tagsByRepository = registryTagsByRepository(images.value);
2704
+ if (!images.failed && tagsByRepository !== undefined) {
2705
+ const present = [];
2706
+ for (const [repository, tags] of tagsByRepository) {
2707
+ if (!ownedRepositories.includes(repository))
2708
+ continue;
2709
+ for (const tag of tags)
2710
+ present.push(`${repository}:${tag}`);
2711
+ }
2712
+ registryTagsPresent = present;
2259
2713
  }
2260
2714
  }
2261
2715
  return {
@@ -2264,21 +2718,22 @@ async function defaultDestroyTargetRead(request) {
2264
2718
  ? {}
2265
2719
  : { containerApplicationId: firstContainer.id }),
2266
2720
  ...(accountId === undefined ? {} : { accountId }),
2267
- ...(registryImagesPresent === undefined
2721
+ ...(registryTagsPresent === undefined
2268
2722
  ? {}
2269
- : { registryImagesPresent }),
2723
+ : { registryTagsPresent }),
2270
2724
  };
2271
2725
  }
2272
2726
  /**
2273
2727
  * `wrangler containers images list --json` reports `[{name, tags}]` entries
2274
- * with the account prefix already stripped from the repository name. The set
2275
- * carries `repository:tag` refs for the owned tags Eden publishes.
2728
+ * with the account prefix already stripped from the repository name. The map
2729
+ * carries every tag per repository so owned-repository cleanup can delete
2730
+ * snapshot tags alongside the pushed `candidate` tag.
2276
2731
  */
2277
- function registryImageNames(value) {
2732
+ function registryTagsByRepository(value) {
2278
2733
  const collection = jsonCollection(value);
2279
2734
  if (collection === undefined)
2280
2735
  return undefined;
2281
- const names = new Set();
2736
+ const tagsByRepository = new Map();
2282
2737
  for (const entry of collection) {
2283
2738
  if (typeof entry !== "object" || entry === null || !("name" in entry)) {
2284
2739
  return undefined;
@@ -2287,13 +2742,15 @@ function registryImageNames(value) {
2287
2742
  const tags = "tags" in entry ? entry.tags : undefined;
2288
2743
  if (typeof name !== "string" || !Array.isArray(tags))
2289
2744
  return undefined;
2745
+ const validTags = [];
2290
2746
  for (const tag of tags) {
2291
2747
  if (typeof tag !== "string")
2292
2748
  return undefined;
2293
- names.add(`${name}:${tag}`);
2749
+ validTags.push(tag);
2294
2750
  }
2751
+ tagsByRepository.set(name, validTags);
2295
2752
  }
2296
- return names;
2753
+ return tagsByRepository;
2297
2754
  }
2298
2755
  async function defaultWorkerDelete(request) {
2299
2756
  const result = await runWranglerWithInput(["delete", "--name", request.name, "--force"], process.cwd(), "y\n");
@@ -2313,23 +2770,59 @@ async function defaultContainerDelete(request) {
2313
2770
  }
2314
2771
  return "indeterminate";
2315
2772
  }
2316
- async function defaultRegistryImageDelete(request) {
2317
- const result = await runWranglerWithInput(["containers", "images", "delete", request.image, "--skip-confirmation"], process.cwd());
2318
- if (result.exitCode === 0)
2319
- return "deleted";
2320
- if (/(?:does not exist|not found|404)/iu.test(result.stderr)) {
2773
+ /**
2774
+ * Deletes every tag in an owned registry repository — the `candidate` tag
2775
+ * Eden pushed plus any `rootfs-snapshot-*`/`rootfs-set-*` tags container
2776
+ * snapshots created under the source image repository. Deleting only
2777
+ * `candidate` would leave snapshot storage behind, so each tag in the
2778
+ * repository is deleted individually.
2779
+ */
2780
+ async function defaultRegistryRepositoryDelete(request) {
2781
+ const images = await readWranglerJson([
2782
+ "containers",
2783
+ "images",
2784
+ "list",
2785
+ "--json",
2786
+ ]);
2787
+ const collection = jsonCollection(images.value);
2788
+ if (images.failed || collection === undefined)
2789
+ return "indeterminate";
2790
+ const entry = collection.find((candidate) => typeof candidate === "object" &&
2791
+ candidate !== null &&
2792
+ candidate.name === request.repository);
2793
+ if (entry === undefined)
2321
2794
  return "absent";
2795
+ const tags = entry.tags;
2796
+ if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string")) {
2797
+ return "indeterminate";
2322
2798
  }
2323
- return "indeterminate";
2799
+ let deletedAny = false;
2800
+ for (const tag of tags) {
2801
+ const result = await runWranglerWithInput([
2802
+ "containers",
2803
+ "images",
2804
+ "delete",
2805
+ `${request.repository}:${tag}`,
2806
+ "--skip-confirmation",
2807
+ ], process.cwd());
2808
+ if (result.exitCode === 0) {
2809
+ deletedAny = true;
2810
+ continue;
2811
+ }
2812
+ if (!/(?:does not exist|not found|404)/iu.test(result.stderr)) {
2813
+ return "indeterminate";
2814
+ }
2815
+ }
2816
+ return deletedAny ? "deleted" : "absent";
2324
2817
  }
2325
2818
  /**
2326
- * The exact `<repository>:${EVE_REGISTRY_IMAGE_TAG}` ref this generation's
2327
- * deployment record proves Eden pushed, or undefined. Ownership holds only
2328
- * when the recorded digest reference points at the repository derived from
2329
- * the same identity that produced it; a digest-bearing reference to any other
2330
- * repository was published outside Eden's push and is never owned here.
2819
+ * The registry repository this generation's deployment record proves Eden
2820
+ * pushed to, or undefined. Ownership holds only when the recorded digest
2821
+ * reference points at the repository derived from the same identity that
2822
+ * produced it; a digest-bearing reference to any other repository was
2823
+ * published outside Eden's push and is never owned here.
2331
2824
  */
2332
- function ownedRegistryImageRef(identity, environment, name, accountId) {
2825
+ function ownedRegistryRepository(identity, environment, name, accountId) {
2333
2826
  if (identity.projectId === undefined ||
2334
2827
  identity.generationId === undefined ||
2335
2828
  identity.containerImage === undefined ||
@@ -2342,14 +2835,14 @@ function ownedRegistryImageRef(identity, environment, name, accountId) {
2342
2835
  imageDigestFromReference(identity.containerImage) === undefined) {
2343
2836
  return undefined;
2344
2837
  }
2345
- return `${repository}:${EVE_REGISTRY_IMAGE_TAG}`;
2838
+ return repository;
2346
2839
  }
2347
2840
  /**
2348
- * The exact image ref an indeterminate deploy attempt provably pushed. The
2349
- * retained attempt evidence names the target's repository; the account and
2350
- * target key must match the verified destroy record.
2841
+ * The registry repository an indeterminate deploy attempt provably pushed.
2842
+ * The retained attempt evidence names the target's repository; the account
2843
+ * and target key must match the verified destroy record.
2351
2844
  */
2352
- async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
2845
+ async function attemptRegistryRepository(attemptPath, accountId, targetKey) {
2353
2846
  const raw = await readFile(attemptPath, "utf8").catch(() => undefined);
2354
2847
  if (raw === undefined)
2355
2848
  return undefined;
@@ -2369,7 +2862,7 @@ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
2369
2862
  (!("targetKey" in parsed) || parsed.targetKey === targetKey) &&
2370
2863
  "accountId" in parsed &&
2371
2864
  parsed.accountId === accountId) {
2372
- return `${parsed.imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
2865
+ return parsed.imageRepository;
2373
2866
  }
2374
2867
  if ("imageReference" in parsed &&
2375
2868
  typeof parsed.imageReference === "string" &&
@@ -2379,39 +2872,40 @@ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
2379
2872
  if (repository !== undefined &&
2380
2873
  repository.startsWith(expected) &&
2381
2874
  (!("targetKey" in parsed) || parsed.targetKey === targetKey)) {
2382
- return `${repository.slice(`registry.cloudflare.com/${accountId}/`.length)}:${EVE_REGISTRY_IMAGE_TAG}`;
2875
+ return repository.slice(`registry.cloudflare.com/${accountId}/`.length);
2383
2876
  }
2384
2877
  }
2385
2878
  return undefined;
2386
2879
  }
2387
2880
  /**
2388
- * Every registry image tag this exact target provably owns: the recorded
2881
+ * Every registry repository this exact target provably owns: the recorded
2389
2882
  * digest references in matching deployment records plus the repository
2390
2883
  * evidence retained by indeterminate pushes. Never derived by prefix-matching
2391
- * the registry; each ref is computed from the immutable generation records.
2884
+ * the registry; each repository is computed from the immutable generation
2885
+ * records.
2392
2886
  */
2393
- async function collectEveDestroyRegistryImages(projectRoot, environment, name, record) {
2887
+ async function collectEveDestroyRegistryRepositories(projectRoot, environment, name, record) {
2394
2888
  const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
2395
2889
  const entries = await readdir(generationsRoot).catch(() => []);
2396
- const images = new Set();
2890
+ const repositories = new Set();
2397
2891
  const targetKey = record.projectId === undefined
2398
2892
  ? undefined
2399
2893
  : stableDeploymentKey(record.projectId, record.accountId, environment, name);
2400
2894
  for (const entry of entries) {
2401
2895
  const generationRoot = join(generationsRoot, entry);
2402
2896
  const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
2403
- const deployedImage = parsed === undefined
2897
+ const deployedRepository = parsed === undefined
2404
2898
  ? undefined
2405
- : ownedRegistryImageRef(parsed.identity, environment, name, record.accountId);
2406
- if (deployedImage !== undefined)
2407
- images.add(deployedImage);
2899
+ : ownedRegistryRepository(parsed.identity, environment, name, record.accountId);
2900
+ if (deployedRepository !== undefined)
2901
+ repositories.add(deployedRepository);
2408
2902
  if (targetKey === undefined)
2409
2903
  continue;
2410
- const attemptImage = await attemptRegistryImageRef(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
2411
- if (attemptImage !== undefined)
2412
- images.add(attemptImage);
2904
+ const attemptRepository = await attemptRegistryRepository(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
2905
+ if (attemptRepository !== undefined)
2906
+ repositories.add(attemptRepository);
2413
2907
  }
2414
- return [...images];
2908
+ return [...repositories];
2415
2909
  }
2416
2910
  export async function runEveDestroy(request, options = {}) {
2417
2911
  const startedAt = Date.now();
@@ -2464,11 +2958,11 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2464
2958
  }
2465
2959
  checks.push(check("VAL-LIFE-006-RECORD", "passed", "The immutable deployment record matched the exact target selectors."));
2466
2960
  eveProgress(options, "verifying the immutable ownership record");
2467
- const ownedRegistryImages = await collectEveDestroyRegistryImages(request.projectRoot, request.environment, request.name, record.identity);
2961
+ const ownedRegistryRepositories = await collectEveDestroyRegistryRepositories(request.projectRoot, request.environment, request.name, record.identity);
2468
2962
  const readTarget = () => Promise.resolve(reader({
2469
2963
  workerName: expectedWorker,
2470
2964
  containerApplicationName: expectedContainer,
2471
- registryImages: ownedRegistryImages,
2965
+ registryRepositories: ownedRegistryRepositories,
2472
2966
  }));
2473
2967
  const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
2474
2968
  eveProgress(options, "reading the exact remote inventory");
@@ -2478,9 +2972,9 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2478
2972
  }
2479
2973
  if (!before.workerExists &&
2480
2974
  before.containerApplicationId === undefined &&
2481
- (ownedRegistryImages.length === 0 ||
2482
- (before.registryImagesPresent !== undefined &&
2483
- before.registryImagesPresent.length === 0))) {
2975
+ (ownedRegistryRepositories.length === 0 ||
2976
+ (before.registryTagsPresent !== undefined &&
2977
+ before.registryTagsPresent.length === 0))) {
2484
2978
  emit({
2485
2979
  ok: true,
2486
2980
  status: "absent",
@@ -2537,17 +3031,22 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2537
3031
  ? "The exact owned Container application was deleted."
2538
3032
  : "The exact Container application was already absent."));
2539
3033
  }
2540
- const imageDeletes = new Map();
2541
- if (ownedRegistryImages.length > 0) {
2542
- const deleteRegistryImage = options.deleteRegistryImage ?? defaultRegistryImageDelete;
2543
- const deletable = before.registryImagesPresent === undefined
2544
- ? ownedRegistryImages
2545
- : ownedRegistryImages.filter((image) => before.registryImagesPresent?.includes(image) === true);
3034
+ const repositoryDeletes = new Map();
3035
+ if (ownedRegistryRepositories.length > 0) {
3036
+ const deleteRegistryRepository = options.deleteRegistryRepository ?? defaultRegistryRepositoryDelete;
3037
+ // Every tag inside an owned repository is deleted (candidate plus
3038
+ // snapshot tags). When the registry could not be enumerated, still
3039
+ // attempt the exact owned repositories; absence verification below
3040
+ // re-reads the registry when it becomes readable.
3041
+ const presentRepositories = new Set((before.registryTagsPresent ?? []).map((ref) => ref.split(":")[0] ?? ""));
3042
+ const deletable = before.registryTagsPresent === undefined
3043
+ ? ownedRegistryRepositories
3044
+ : ownedRegistryRepositories.filter((repository) => presentRepositories.has(repository));
2546
3045
  if (deletable.length > 0) {
2547
- eveProgress(options, `deleting ${deletable.length} registry image${deletable.length === 1 ? "" : "s"}`);
3046
+ eveProgress(options, `deleting ${deletable.length} registry repositor${deletable.length === 1 ? "y" : "ies"}`);
2548
3047
  }
2549
- for (const image of deletable) {
2550
- imageDeletes.set(image, await deleteRegistryImage({ image }));
3048
+ for (const repository of deletable) {
3049
+ repositoryDeletes.set(repository, await deleteRegistryRepository({ repository }));
2551
3050
  }
2552
3051
  }
2553
3052
  eveProgress(options, "verifying the exact target is absent");
@@ -2565,15 +3064,16 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2565
3064
  });
2566
3065
  fail("EVE_DESTROY_ABSENCE_UNPROVEN", "The exact target could not be proven absent after deletion.");
2567
3066
  }
2568
- const retainedImages = ownedRegistryImages.filter((image) => after.registryImagesPresent === undefined
2569
- ? imageDeletes.get(image) === "indeterminate"
2570
- : after.registryImagesPresent.includes(image));
2571
- if (ownedRegistryImages.length > 0) {
2572
- checks.push(retainedImages.length === 0
3067
+ const presentTagsAfter = after.registryTagsPresent ?? [];
3068
+ const retainedRepositories = ownedRegistryRepositories.filter((repository) => presentTagsAfter.some((ref) => ref.startsWith(`${repository}:`)) ||
3069
+ (after.registryTagsPresent === undefined &&
3070
+ repositoryDeletes.get(repository) === "indeterminate"));
3071
+ if (ownedRegistryRepositories.length > 0) {
3072
+ checks.push(retainedRepositories.length === 0
2573
3073
  ? check("VAL-LIFE-006-REGISTRY", "passed", "Every registry image tag recorded for this exact target was deleted or proven absent.")
2574
- : check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image(s) remain after destroy: ${retainedImages.join(", ")}.`, "Retry eden destroy with the same selectors, or remove only the listed image refs with wrangler containers images delete."));
3074
+ : check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image tag(s) remain after destroy: ${retainedRepositories.join(", ")}.`, "Retry eden destroy with the same selectors, or remove only the listed repositories' tags with wrangler containers images delete."));
2575
3075
  }
2576
- if (retainedImages.length > 0) {
3076
+ if (retainedRepositories.length > 0) {
2577
3077
  emit({
2578
3078
  ok: false,
2579
3079
  status: "failed",
@@ -2581,7 +3081,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2581
3081
  name: request.name,
2582
3082
  checks,
2583
3083
  });
2584
- fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image(s) remain: ${retainedImages.join(", ")}.`);
3084
+ fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image tag(s) remain in: ${retainedRepositories.join(", ")}.`);
2585
3085
  }
2586
3086
  checks.push(check("VAL-LIFE-006", "passed", "Exact absence was verified; clearing this target's CURRENT pointer only."));
2587
3087
  const currentPath = join(request.projectRoot, ".eden", "eve-deploy", "targets", `${request.environment}-${request.name}`, "CURRENT");