@moinulmoin/eden 0.9.0 → 0.10.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.
package/README.md CHANGED
@@ -12,15 +12,15 @@ If you already have an Eve project, start with `eden deploy`.
12
12
  Use any one of these package managers:
13
13
 
14
14
  ```sh
15
- npm install --global @moinulmoin/eden@0.9.0
15
+ npm install --global @moinulmoin/eden@0.10.0
16
16
  ```
17
17
 
18
18
  ```sh
19
- pnpm add --global @moinulmoin/eden@0.9.0
19
+ pnpm add --global @moinulmoin/eden@0.10.0
20
20
  ```
21
21
 
22
22
  ```sh
23
- bun add --global @moinulmoin/eden@0.9.0
23
+ bun add --global @moinulmoin/eden@0.10.0
24
24
  ```
25
25
 
26
26
  Then confirm the CLI is available:
@@ -128,6 +128,11 @@ export interface EveDeploymentIdentity {
128
128
  readonly sourceDigest: string;
129
129
  readonly generationId: string;
130
130
  readonly deploymentId: string;
131
+ readonly releaseId?: string;
132
+ readonly eveVersion?: string;
133
+ readonly affinityContract?: number;
134
+ readonly retainedDeployments?: Readonly<Record<string, EveAffinityDeployment>>;
135
+ readonly pendingRetirements?: Readonly<Record<string, EveAffinityDeployment>>;
131
136
  readonly environment: EveCliEnvironment;
132
137
  readonly name: string;
133
138
  readonly accountId: string;
@@ -144,6 +149,28 @@ export type EveDeploymentIdentityProof = Partial<EveDeploymentIdentity> & {
144
149
  /** The deployment the serving Container verifiably started under; set only by a deployed Eden host that proves its running image before stamping. */
145
150
  readonly startedDeploymentId?: string;
146
151
  };
152
+ export interface EveAffinityDeployment {
153
+ readonly image: string;
154
+ readonly generationId: string;
155
+ readonly runtimeRevisionHandle?: string;
156
+ readonly eveVersion?: string;
157
+ readonly affinityContract?: number;
158
+ readonly releaseId?: string;
159
+ readonly worldRpcAuth?: boolean;
160
+ }
161
+ export interface EveDeploymentDrainState {
162
+ readonly liveRuns: number;
163
+ readonly liveSessions: number;
164
+ readonly pendingMessages: number;
165
+ readonly unfinishedTransfers: number;
166
+ readonly drained: boolean;
167
+ }
168
+ export type EveDeploymentRpcRunner = (request: {
169
+ readonly origin: string;
170
+ readonly operation: "deployments.drainState" | "deployments.retire";
171
+ readonly deploymentIds: readonly string[];
172
+ readonly controlToken: string;
173
+ }) => Promise<unknown>;
147
174
  export interface EveDeploymentPublicationRequest {
148
175
  readonly identity: EveDeploymentIdentity;
149
176
  readonly hostConfig: EveHostConfig;
@@ -233,6 +260,7 @@ export interface EvePreflightOptions {
233
260
  readonly protectedStore?: EveRuntimeProtectedStore;
234
261
  readonly publish?: EveDeploymentPublicationRunner;
235
262
  readonly publishImage?: EveImagePublicationRunner;
263
+ readonly deploymentRpc?: EveDeploymentRpcRunner;
236
264
  readonly compensate?: EveDeploymentCompensationRunner;
237
265
  readonly health?: EveDeploymentHealthRunner;
238
266
  readonly afterPromotion?: () => void | Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { setTimeout as waitForActivation } from "node:timers/promises";
1
2
  import { createRequire, } from "node:module";
2
3
  import { createHash, randomUUID, } from "node:crypto";
3
4
  import { join, dirname, relative, } from "node:path";
@@ -11,7 +12,7 @@ import { EveCliError, EVE_NAME_PATTERN, } from "./eve.js";
11
12
  import { buildEveProjectSnapshot, checkEveSandboxProvider, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, IMAGE_ID_PATTERN, } from "./eve-packaging.js";
12
13
  import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
13
14
  import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
14
- import { createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
15
+ import { EDEN_AFFINITY_CONTRACT, callDeploymentControlRpc, createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
15
16
  const execFileAsync = promisify(execFile);
16
17
  const require = createRequire(import.meta.url);
17
18
  const wranglerEntrypoint = require.resolve("wrangler");
@@ -32,6 +33,54 @@ export const DEFAULT_EVE_NODE_IMAGE = Object.freeze({
32
33
  });
33
34
  /** The single tag Eden pushes to the managed Cloudflare registry per generation. */
34
35
  const EVE_REGISTRY_IMAGE_TAG = "candidate";
36
+ function validateDeploymentDrainStates(value, ids) {
37
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
38
+ throw deploymentFailure("EVE_AFFINITY_DRAIN_UNPROVEN", "The authenticated World did not return deployment drain states.");
39
+ }
40
+ const states = value;
41
+ for (const id of ids) {
42
+ const state = states[id];
43
+ if (typeof state !== "object" || state === null ||
44
+ typeof state.drained !== "boolean" ||
45
+ [state.liveRuns, state.liveSessions, state.pendingMessages, state.unfinishedTransfers]
46
+ .some((count) => !Number.isSafeInteger(count) || count < 0) ||
47
+ state.drained !== (state.liveRuns + state.liveSessions + state.pendingMessages + state.unfinishedTransfers === 0)) {
48
+ throw deploymentFailure("EVE_AFFINITY_DRAIN_UNPROVEN", `The authenticated World returned an invalid drain state for ${id}.`);
49
+ }
50
+ }
51
+ return states;
52
+ }
53
+ const defaultDeploymentRpc = async (request) => {
54
+ // A successful Cloudflare secret write creates an active deployment, but
55
+ // existing stateless Worker isolates may briefly serve the preceding secret.
56
+ // Only the first read-only drain call following our own rotation may wait
57
+ // for that exact in-memory credential. All other failures remain terminal.
58
+ const activationRead = request.operation === "deployments.drainState";
59
+ const deadline = Date.now() + 60_000;
60
+ for (;;) {
61
+ const remaining = deadline - Date.now();
62
+ if (remaining <= 0) {
63
+ throw deploymentFailure("EVE_AFFINITY_CONTROL_ACTIVATION_TIMEOUT", "The freshly rotated deployment-control credential did not activate within 60 seconds; publication was refused.");
64
+ }
65
+ try {
66
+ return await callDeploymentControlRpc({
67
+ origin: request.origin,
68
+ token: request.controlToken,
69
+ operation: request.operation,
70
+ args: [{ deploymentIds: request.deploymentIds }],
71
+ signal: AbortSignal.timeout(remaining),
72
+ });
73
+ }
74
+ catch (error) {
75
+ if (!activationRead || typeof error !== "object" || error === null ||
76
+ !("status" in error) || error.status !== 401)
77
+ throw error;
78
+ const delay = Math.min(1_000, deadline - Date.now());
79
+ if (delay > 0)
80
+ await waitForActivation(delay);
81
+ }
82
+ }
83
+ };
35
84
  function safeText(value) {
36
85
  return redactEveRuntimeOutput(value)
37
86
  .replace(/(?:^|[\s"'=])(?:Bearer|Token)\s+\S+/giu, "$1[redacted]")
@@ -471,13 +520,13 @@ function safeDockerEnvironment() {
471
520
  DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
472
521
  };
473
522
  }
474
- async function readWranglerJson(args) {
523
+ async function readWranglerJson(args, timeoutMs = 30_000) {
475
524
  try {
476
525
  const result = await execFileAsync(process.execPath, [wranglerEntrypoint, ...args], {
477
526
  env: safeWranglerEnvironment(),
478
527
  cwd: wranglerCwd,
479
528
  maxBuffer: 2 * 1024 * 1024,
480
- timeout: 30_000,
529
+ timeout: timeoutMs,
481
530
  });
482
531
  const parsed = JSON.parse(result.stdout.trim());
483
532
  return { value: parsed, failed: false };
@@ -813,11 +862,33 @@ async function resolveEveTargetOwnership(request, result) {
813
862
  evidence.containerNamespaceId === evidence.workerContainerNamespaceId);
814
863
  const remoteProven = evidence !== undefined && workerProven && containerProven;
815
864
  if (!remoteProven) {
865
+ const failures = [];
866
+ if (evidence === undefined)
867
+ failures.push("provider evidence unavailable");
868
+ if (result.target.observed?.worker === true) {
869
+ if (evidence?.settingsReadable !== true)
870
+ failures.push("Worker settings unreadable");
871
+ else if (evidence.workerDeploymentId === undefined)
872
+ failures.push("Worker deployment stamp absent");
873
+ else if (!recordedDeploymentIds.has(evidence.workerDeploymentId))
874
+ failures.push("Worker deployment stamp differs from recorded identities");
875
+ }
876
+ if (result.target.observed?.container === true) {
877
+ if (evidence?.containerSchedulingPolicy !== "durable_object")
878
+ failures.push("Container scheduling policy unproven");
879
+ if (evidence?.containerNamespaceId === undefined)
880
+ failures.push("Container namespace unreadable");
881
+ if (evidence?.workerContainerNamespaceId === undefined)
882
+ failures.push("Worker Container binding namespace unreadable");
883
+ if (evidence?.containerNamespaceId !== undefined && evidence.workerContainerNamespaceId !== undefined &&
884
+ evidence.containerNamespaceId !== evidence.workerContainerNamespaceId)
885
+ failures.push("Container and Worker binding namespaces differ");
886
+ }
816
887
  return {
817
888
  ...result,
818
889
  target: {
819
890
  ...result.target,
820
- 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.",
891
+ 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. Unproven: ${failures.join("; ")}.`,
821
892
  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.",
822
893
  },
823
894
  };
@@ -995,11 +1066,17 @@ function transportIdentityMatches(expected, observed) {
995
1066
  "generationId",
996
1067
  "stableWorkersDevOrigin",
997
1068
  "workerName",
998
- "stableContainerInstanceName",
999
1069
  ];
1000
1070
  if (!required.every((key) => observed[key] === expected[key])) {
1001
1071
  return false;
1002
1072
  }
1073
+ const expectedInstance = expected.retainedDeployments === undefined ||
1074
+ expected.deploymentId === "eden-cloudflare"
1075
+ ? expected.stableContainerInstanceName
1076
+ : `${expected.stableContainerInstanceName}:${expected.deploymentId}`;
1077
+ if (observed.stableContainerInstanceName !== expectedInstance ||
1078
+ observed.releaseId !== expected.releaseId)
1079
+ return false;
1003
1080
  return expected.runtimeRevisionHandle === undefined
1004
1081
  ? observed.runtimeRevisionHandle === undefined
1005
1082
  : observed.runtimeRevisionHandle === expected.runtimeRevisionHandle;
@@ -1012,6 +1089,7 @@ function identityMatches(expected, observed) {
1012
1089
  "sourceDigest",
1013
1090
  "generationId",
1014
1091
  "deploymentId",
1092
+ "releaseId",
1015
1093
  "environment",
1016
1094
  "name",
1017
1095
  "accountId",
@@ -1039,6 +1117,11 @@ function safeIdentity(identity) {
1039
1117
  sourceDigest: identity.sourceDigest,
1040
1118
  generationId: identity.generationId,
1041
1119
  deploymentId: identity.deploymentId,
1120
+ releaseId: identity.releaseId,
1121
+ eveVersion: identity.eveVersion,
1122
+ affinityContract: identity.affinityContract,
1123
+ retainedDeployments: identity.retainedDeployments,
1124
+ pendingRetirements: identity.pendingRetirements,
1042
1125
  environment: identity.environment,
1043
1126
  name: identity.name,
1044
1127
  accountId: identity.accountId,
@@ -1281,6 +1364,7 @@ function responseIdentity(response, body) {
1281
1364
  read("sourceDigest", "x-eden-eve-source-digest");
1282
1365
  read("generationId", "x-eden-eve-generation-id");
1283
1366
  read("deploymentId", "x-eden-eve-deployment-id");
1367
+ read("releaseId", "x-eden-eve-release-id");
1284
1368
  read("environment", "x-eden-eve-environment");
1285
1369
  read("name", "x-eden-eve-name");
1286
1370
  read("accountId", "x-eden-eve-account-id");
@@ -1341,7 +1425,7 @@ function defaultHealthRunner() {
1341
1425
  // must wait until the *served container* verifiably runs this
1342
1426
  // generation's image (the host stamps the header only after
1343
1427
  // inspecting the running instance).
1344
- if (observedIdentity.startedDeploymentId !== request.identity.deploymentId) {
1428
+ if (observedIdentity.startedDeploymentId !== (request.identity.releaseId ?? request.identity.deploymentId)) {
1345
1429
  lastReason =
1346
1430
  "The served Container has not verifiably started this deployment's image yet; waiting for the rollout to land.";
1347
1431
  await wait();
@@ -1460,8 +1544,8 @@ async function promoteEveTargetPointer(deploymentRoot, pointerKey, generationRoo
1460
1544
  }
1461
1545
  return currentPath;
1462
1546
  }
1463
- async function writeEveDeploymentRecord(candidate, identity, hostConfig, status, extra = {}) {
1464
- await writeFile(join(candidate.generationRoot, "deployment.json"), `${JSON.stringify({
1547
+ async function writeEveDeploymentRecord(candidate, identity, hostConfig, status, extra = {}, filename = "deployment.json") {
1548
+ await writeFile(join(candidate.generationRoot, filename), `${JSON.stringify({
1465
1549
  version: 1,
1466
1550
  status,
1467
1551
  identity: safeIdentity(identity),
@@ -1474,6 +1558,10 @@ async function writeEveDeploymentRecord(candidate, identity, hostConfig, status,
1474
1558
  flag: "wx",
1475
1559
  });
1476
1560
  }
1561
+ async function deploymentRecordPath(generationRoot) {
1562
+ const path = join(generationRoot, "deployment.json");
1563
+ return await lstat(path).then(() => path, () => join(generationRoot, "deployment-intent.json"));
1564
+ }
1477
1565
  async function writeEveIndeterminateEvidence(candidate, evidence, extra = {}) {
1478
1566
  await writeFile(join(candidate.generationRoot, "deployment-attempt.json"), `${JSON.stringify({
1479
1567
  version: 1,
@@ -1538,6 +1626,14 @@ async function runEveDeployment(request, options, collected) {
1538
1626
  }
1539
1627
  const priorIdentity = prior?.identity;
1540
1628
  const priorDurableObjects = prior?.durableObjects;
1629
+ const worldManifest = collected.workflowWorld === "@moinulmoin/eden-world-cloudflare"
1630
+ ? await readInstalledManifest(candidate.snapshotRoot, collected.workflowWorld)
1631
+ : undefined;
1632
+ const affinityEnabled = worldManifest?.eden?.capabilities?.deploymentAffinity === true;
1633
+ const worldRpcAuth = worldManifest?.eden?.capabilities?.worldRpcAuth === true;
1634
+ if (!affinityEnabled && priorIdentity?.retainedDeployments !== undefined) {
1635
+ throw deploymentFailure("EVE_AFFINITY_REQUIRED", "This target retains affinity deployments; install a World package declaring deploymentAffinity before updating it.");
1636
+ }
1541
1637
  const superseded = isUpdate
1542
1638
  ? {
1543
1639
  generationId: priorIdentity?.generationId,
@@ -1553,8 +1649,12 @@ async function runEveDeployment(request, options, collected) {
1553
1649
  if (durableWorld) {
1554
1650
  resultWarnings.push({
1555
1651
  id: "EVE_UPDATE_IN_FLIGHT",
1556
- 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.",
1557
- remediation: "Let pending approvals and long runs settle before updating, or keep step names, queue names, and serialized payloads compatible across the change.",
1652
+ message: affinityEnabled
1653
+ ? "This update retains pinned deployments until their sessions, queue messages, and transfers drain."
1654
+ : "This update replaces the running Container; an Eve version change can strand existing sessions because the installed World does not declare deployment affinity.",
1655
+ remediation: affinityEnabled
1656
+ ? "End old sessions to allow their deployment images to retire."
1657
+ : "Let sessions settle before changing Eve versions, or install an affinity-capable World package.",
1558
1658
  });
1559
1659
  }
1560
1660
  if (prior?.worldCloudflare !==
@@ -1595,6 +1695,80 @@ async function runEveDeployment(request, options, collected) {
1595
1695
  throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not provide an immutable runtime image digest.");
1596
1696
  }
1597
1697
  const targetKey = stableDeploymentKey(collected.projectId, accountId, request.environment, request.name);
1698
+ const releaseId = `eve-deploy-${randomUUID()}`;
1699
+ let previousEveVersion = priorIdentity?.eveVersion;
1700
+ if (previousEveVersion === undefined && prior !== undefined) {
1701
+ previousEveVersion = (await readInstalledManifest(join(prior.generationRoot, "container/snapshot"), "eve"))?.version;
1702
+ }
1703
+ const retainedDeployments = {
1704
+ ...priorIdentity?.retainedDeployments,
1705
+ };
1706
+ if (affinityEnabled && priorIdentity?.retainedDeployments === undefined &&
1707
+ priorIdentity?.containerImage !== undefined && priorIdentity.generationId !== undefined) {
1708
+ retainedDeployments["eden-cloudflare"] = {
1709
+ image: priorIdentity.containerImage,
1710
+ generationId: priorIdentity.generationId,
1711
+ ...(previousEveVersion === undefined ? {} : { eveVersion: previousEveVersion }),
1712
+ affinityContract: EDEN_AFFINITY_CONTRACT,
1713
+ ...(priorIdentity.runtimeRevisionHandle === undefined ? {} : { runtimeRevisionHandle: priorIdentity.runtimeRevisionHandle }),
1714
+ };
1715
+ }
1716
+ const matchingDeployment = Object.entries(retainedDeployments).find(([, entry]) => entry.eveVersion === candidate.eveVersion && entry.affinityContract === EDEN_AFFINITY_CONTRACT)?.[0];
1717
+ const deploymentId = !affinityEnabled ? releaseId : matchingDeployment ??
1718
+ `eden-affinity-${createHash("sha256").update(`${candidate.eveVersion}:${EDEN_AFFINITY_CONTRACT}`).digest("hex").slice(0, 24)}`;
1719
+ const retiringDeployments = { ...priorIdentity?.pendingRetirements };
1720
+ delete retiringDeployments[deploymentId];
1721
+ if (priorIdentity?.deploymentId !== undefined)
1722
+ delete retiringDeployments[priorIdentity.deploymentId];
1723
+ for (const id of prior?.protectedDeploymentIds ?? [])
1724
+ delete retiringDeployments[id];
1725
+ const hasRetainedNoncurrentDeployments = priorIdentity?.retainedDeployments !== undefined &&
1726
+ Object.keys(retainedDeployments).some((id) => id !== priorIdentity.deploymentId);
1727
+ const controlToken = `${randomUUID()}${randomUUID()}`;
1728
+ if (affinityEnabled && (hasRetainedNoncurrentDeployments || Object.keys(retiringDeployments).length > 0)) {
1729
+ await (options.protectedStore ?? defaultProtectedStore(request.name)).put({
1730
+ targetId: targetKey, revision: `eve-affinity-control-${randomUUID()}`,
1731
+ inputIdentity: {
1732
+ token: `eve-affinity-control-input-${randomUUID()}`,
1733
+ byteLength: Buffer.byteLength(controlToken),
1734
+ digest: createHash("sha256").update(controlToken).digest("hex"),
1735
+ },
1736
+ variableNames: ["EDEN_EVE_CONTROL_TOKEN"], values: { EDEN_EVE_CONTROL_TOKEN: controlToken },
1737
+ });
1738
+ }
1739
+ if (affinityEnabled && hasRetainedNoncurrentDeployments) {
1740
+ const ids = Object.keys(retainedDeployments);
1741
+ const raw = await (options.deploymentRpc ?? defaultDeploymentRpc)({
1742
+ origin: stableWorkersDevOrigin, operation: "deployments.drainState", deploymentIds: ids,
1743
+ controlToken,
1744
+ });
1745
+ const states = validateDeploymentDrainStates(raw, ids);
1746
+ const eligible = ids.filter((id) => id !== deploymentId && id !== priorIdentity.deploymentId &&
1747
+ !prior?.protectedDeploymentIds?.includes(id) && states[id]?.drained === true);
1748
+ const remainingCount = ids.length - eligible.length + (deploymentId in retainedDeployments ? 0 : 1);
1749
+ if (remainingCount <= 10 && eligible.length > 0) {
1750
+ // Drain snapshots are not fences. Retire only noncurrent targets, and
1751
+ // obtain the World's durable admission fence before dropping routes.
1752
+ const retired = await (options.deploymentRpc ?? defaultDeploymentRpc)({
1753
+ origin: stableWorkersDevOrigin, operation: "deployments.retire",
1754
+ deploymentIds: eligible, controlToken,
1755
+ });
1756
+ if (typeof retired !== "object" || retired === null || !("retired" in retired) ||
1757
+ !Array.isArray(retired.retired) ||
1758
+ eligible.some((id) => !retired.retired.includes(id))) {
1759
+ throw deploymentFailure("EVE_AFFINITY_RETIRE_UNPROVEN", "The World did not prove the noncurrent deployments durably fenced for retirement.");
1760
+ }
1761
+ for (const id of eligible) {
1762
+ retiringDeployments[id] = retainedDeployments[id];
1763
+ delete retainedDeployments[id];
1764
+ }
1765
+ }
1766
+ if (Object.keys(retainedDeployments).length + (deploymentId in retainedDeployments ? 0 : 1) > 10) {
1767
+ const blockers = Object.entries(states).filter(([id]) => id in retainedDeployments)
1768
+ .map(([id, state]) => `${id}: ${state.liveSessions} live sessions, ${state.liveRuns} live runs, ${state.pendingMessages} pending messages, ${state.unfinishedTransfers} unfinished transfers`);
1769
+ throw deploymentFailure("EVE_AFFINITY_LIMIT", `Deployment refused before image/code publication: at most 10 affinity deployments may be retained. Blocking deployments: ${blockers.join("; ")}`);
1770
+ }
1771
+ }
1598
1772
  let imageReference = options.containerImageReference;
1599
1773
  if (imageReference !== undefined) {
1600
1774
  if (imageDigestFromReference(imageReference) !== imageDigest) {
@@ -1607,7 +1781,7 @@ async function runEveDeployment(request, options, collected) {
1607
1781
  if (localImageReference === undefined || localImageReference.length === 0) {
1608
1782
  throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not return an exact local image handle for publication.");
1609
1783
  }
1610
- const imageRepository = `eden-eve-${targetKey}-${candidate.generationId}`;
1784
+ const imageRepository = `eden-eve-${targetKey}-${affinityEnabled ? deploymentId : candidate.generationId}`;
1611
1785
  const imageTag = `${imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
1612
1786
  const targetImageReference = `registry.cloudflare.com/${accountId}/${imageRepository}@${imageDigest}`;
1613
1787
  const imagePublication = await (options.publishImage ??
@@ -1669,7 +1843,6 @@ async function runEveDeployment(request, options, collected) {
1669
1843
  throw deploymentFailure("DEPLOY_INDETERMINATE", reason);
1670
1844
  }
1671
1845
  }
1672
- const deploymentId = `eve-deploy-${randomUUID()}`;
1673
1846
  const containerApplicationName = boundedResourceName(request.name, "container");
1674
1847
  const stableContainerInstanceName = boundedResourceName(request.name, "instance");
1675
1848
  const runtimeVariableNames = candidate.runtimeVariableNames;
@@ -1678,6 +1851,24 @@ async function runEveDeployment(request, options, collected) {
1678
1851
  sourceDigest: candidate.sourceDigest,
1679
1852
  generationId: candidate.generationId,
1680
1853
  deploymentId,
1854
+ releaseId,
1855
+ eveVersion: candidate.eveVersion,
1856
+ ...(affinityEnabled ? {
1857
+ affinityContract: EDEN_AFFINITY_CONTRACT,
1858
+ retainedDeployments: {
1859
+ ...retainedDeployments,
1860
+ [deploymentId]: {
1861
+ image: imageReference,
1862
+ generationId: candidate.generationId,
1863
+ eveVersion: candidate.eveVersion,
1864
+ affinityContract: EDEN_AFFINITY_CONTRACT,
1865
+ releaseId,
1866
+ worldRpcAuth,
1867
+ ...(runtimeInjection?.protectedHandle === undefined ? {} : { runtimeRevisionHandle: runtimeInjection.protectedHandle }),
1868
+ },
1869
+ },
1870
+ pendingRetirements: retiringDeployments,
1871
+ } : {}),
1681
1872
  environment: request.environment,
1682
1873
  name: request.name,
1683
1874
  accountId,
@@ -1698,6 +1889,11 @@ async function runEveDeployment(request, options, collected) {
1698
1889
  containerApplicationName: identity.containerApplicationName,
1699
1890
  stableContainerInstanceName: identity.stableContainerInstanceName,
1700
1891
  deploymentId: identity.deploymentId,
1892
+ worldRpcAuth,
1893
+ ...(identity.releaseId === undefined ? {} : { releaseId: identity.releaseId }),
1894
+ ...(identity.eveVersion === undefined ? {} : { eveVersion: identity.eveVersion }),
1895
+ ...(identity.affinityContract === undefined ? {} : { affinityContract: identity.affinityContract }),
1896
+ ...(identity.retainedDeployments === undefined ? {} : { retainedDeployments: identity.retainedDeployments }),
1701
1897
  generationId: identity.generationId,
1702
1898
  stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
1703
1899
  containerImage: identity.containerImage,
@@ -1753,6 +1949,7 @@ async function runEveDeployment(request, options, collected) {
1753
1949
  throw error;
1754
1950
  }
1755
1951
  };
1952
+ await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: "publication-intent" }, "deployment-intent.json");
1756
1953
  try {
1757
1954
  eveProgress(options, "publishing Worker and Container application");
1758
1955
  publication = await publishOne(publicationRequest);
@@ -1955,7 +2152,40 @@ async function runEveDeployment(request, options, collected) {
1955
2152
  await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), `${request.environment}-${request.name}`, candidate.generationRoot);
1956
2153
  await options.afterPromotion?.();
1957
2154
  let supersededImageCleanup;
1958
- if (isUpdate) {
2155
+ if (affinityEnabled && Object.keys(retiringDeployments).length > 0) {
2156
+ const ids = Object.keys(retiringDeployments);
2157
+ const completedRetirements = [];
2158
+ try {
2159
+ const result = await (options.deploymentRpc ?? defaultDeploymentRpc)({
2160
+ origin: stableWorkersDevOrigin, operation: "deployments.retire", deploymentIds: ids,
2161
+ controlToken,
2162
+ });
2163
+ if (typeof result !== "object" || result === null ||
2164
+ !("retired" in result) || !Array.isArray(result.retired) ||
2165
+ ids.some((id) => !result.retired.includes(id)))
2166
+ throw new Error("Retirement was not proven.");
2167
+ for (const [id, retained] of Object.entries(retiringDeployments)) {
2168
+ const repository = ownedRegistryRepository({
2169
+ ...identity, deploymentId: id, containerImage: retained.image, generationId: retained.generationId,
2170
+ }, request.environment, request.name, accountId);
2171
+ if (repository !== undefined && await (options.deleteRegistryRepository ??
2172
+ defaultRegistryRepositoryDelete)({ repository }) === "indeterminate") {
2173
+ throw new Error(`Repository deletion was not proven: ${repository}`);
2174
+ }
2175
+ completedRetirements.push(id);
2176
+ }
2177
+ supersededImageCleanup = check("VAL-LIFE-007", "passed", "Drained deployment instances and their owned registry repositories were retired.");
2178
+ }
2179
+ catch {
2180
+ supersededImageCleanup = check("VAL-LIFE-007", "failed", "The deployment is healthy, but drained resource retirement was not proven; exact pending retirement records were retained for retry.", "Retry deploy to finish only recorded drained resource cleanup.");
2181
+ }
2182
+ if (completedRetirements.length > 0) {
2183
+ // Keep the immutable deployment record as evidence, but exclude proven
2184
+ // cleanup from the next deploy's active retry set.
2185
+ await writeFile(join(candidate.generationRoot, "completed-retirements.json"), `${JSON.stringify(completedRetirements)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
2186
+ }
2187
+ }
2188
+ if (isUpdate && !affinityEnabled) {
1959
2189
  // The superseded generation's registry repository is generation-scoped
1960
2190
  // and no longer referenced once CURRENT promotes this generation; only
1961
2191
  // then is it safe to delete. Every tag in the repository goes with it —
@@ -2512,7 +2742,7 @@ async function readEveDestroyRecord(projectRoot, environment, name) {
2512
2742
  if (generationRoot === undefined) {
2513
2743
  return readEveDestroyRecordFromGenerations(projectRoot, environment, name);
2514
2744
  }
2515
- const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
2745
+ const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
2516
2746
  return parsed === undefined
2517
2747
  ? undefined
2518
2748
  : { ...parsed, generationRoot };
@@ -2526,7 +2756,7 @@ async function readEveDestroyRecordFromGenerations(projectRoot, environment, nam
2526
2756
  const details = await lstat(generationRoot).catch(() => undefined);
2527
2757
  if (details === undefined || !details.isDirectory())
2528
2758
  continue;
2529
- const recordDetails = await lstat(join(generationRoot, "deployment.json")).catch(() => undefined);
2759
+ const recordDetails = await lstat(await deploymentRecordPath(generationRoot)).catch(() => undefined);
2530
2760
  if (recordDetails !== undefined && recordDetails.isFile()) {
2531
2761
  withRecords.push({
2532
2762
  generationRoot,
@@ -2536,7 +2766,7 @@ async function readEveDestroyRecordFromGenerations(projectRoot, environment, nam
2536
2766
  }
2537
2767
  withRecords.sort((left, right) => right.modifiedAt - left.modifiedAt);
2538
2768
  for (const { generationRoot } of withRecords) {
2539
- const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
2769
+ const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
2540
2770
  if (parsed !== undefined) {
2541
2771
  return { ...parsed, generationRoot };
2542
2772
  }
@@ -2581,7 +2811,35 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
2581
2811
  if (identity.environment !== environment || identity.name !== name) {
2582
2812
  return undefined;
2583
2813
  }
2814
+ const metadata = {};
2815
+ for (const key of ["deploymentId", "eveVersion", "releaseId", "runtimeRevisionHandle"]) {
2816
+ if (typeof identity[key] === "string")
2817
+ Object.assign(metadata, { [key]: identity[key] });
2818
+ }
2819
+ if (typeof identity.affinityContract === "number")
2820
+ Object.assign(metadata, { affinityContract: identity.affinityContract });
2821
+ for (const key of ["retainedDeployments", "pendingRetirements"]) {
2822
+ const value = identity[key];
2823
+ if (value === undefined)
2824
+ continue;
2825
+ if (typeof value !== "object" || value === null || Array.isArray(value))
2826
+ return undefined;
2827
+ for (const [id, deployment] of Object.entries(value)) {
2828
+ if (!/^(?:eden-cloudflare|eden-affinity-[a-f0-9]{24})$/u.test(id) ||
2829
+ typeof deployment !== "object" || deployment === null ||
2830
+ typeof deployment.image !== "string" || imageDigestFromReference(deployment.image) === undefined ||
2831
+ typeof deployment.generationId !== "string" ||
2832
+ (deployment.eveVersion !== undefined && typeof deployment.eveVersion !== "string") ||
2833
+ (deployment.affinityContract !== undefined && typeof deployment.affinityContract !== "number") ||
2834
+ (deployment.releaseId !== undefined && typeof deployment.releaseId !== "string") ||
2835
+ (deployment.worldRpcAuth !== undefined && typeof deployment.worldRpcAuth !== "boolean") ||
2836
+ (deployment.runtimeRevisionHandle !== undefined && typeof deployment.runtimeRevisionHandle !== "string"))
2837
+ return undefined;
2838
+ }
2839
+ Object.assign(metadata, { [key]: value });
2840
+ }
2584
2841
  const verified = {
2842
+ ...metadata,
2585
2843
  workerName: identity.workerName,
2586
2844
  containerApplicationName: identity.containerApplicationName,
2587
2845
  stableContainerInstanceName: identity.stableContainerInstanceName,
@@ -2620,7 +2878,7 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2620
2878
  const deploymentIds = new Set();
2621
2879
  for (const entry of entries) {
2622
2880
  const generationRoot = join(generationsRoot, entry);
2623
- const recordPath = join(generationRoot, "deployment.json");
2881
+ const recordPath = await deploymentRecordPath(generationRoot);
2624
2882
  const details = await lstat(recordPath).catch(() => undefined);
2625
2883
  if (details === undefined || !details.isFile())
2626
2884
  continue;
@@ -2653,7 +2911,7 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2653
2911
  live = pinned;
2654
2912
  }
2655
2913
  else {
2656
- const parsed = await parseEveDeploymentRecord(join(generationRoot, "deployment.json"), environment, name);
2914
+ const parsed = await parseEveDeploymentRecord(await deploymentRecordPath(generationRoot), environment, name);
2657
2915
  if (parsed !== undefined &&
2658
2916
  !((accountId !== undefined &&
2659
2917
  parsed.identity.accountId !== accountId) ||
@@ -2669,10 +2927,31 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2669
2927
  // Newest record carrying a usable DO history wins, regardless of its
2670
2928
  // deployment outcome: a tag it published must never be replayed.
2671
2929
  const historySource = scanned.find((entry) => entry.record.durableObjects !== undefined);
2930
+ // CURRENT is only a health/promotion pointer. A newer uncertain publication
2931
+ // may already serve sessions; preserve every such image until a later deploy
2932
+ // can prove it drained, never infer absence from a failed health check.
2933
+ const unresolved = scanned.filter((entry) => entry.modifiedAt > live.modifiedAt);
2934
+ const retainedDeployments = { ...live.record.identity.retainedDeployments };
2935
+ const pendingRetirements = { ...live.record.identity.pendingRetirements };
2936
+ const protectedDeploymentIds = new Set();
2937
+ if (live.record.identity.retainedDeployments === undefined) {
2938
+ protectedDeploymentIds.add("eden-cloudflare");
2939
+ }
2940
+ for (const entry of unresolved.slice().reverse()) {
2941
+ Object.assign(retainedDeployments, entry.record.identity.retainedDeployments);
2942
+ Object.assign(pendingRetirements, entry.record.identity.pendingRetirements);
2943
+ if (entry.record.deploymentId !== undefined)
2944
+ protectedDeploymentIds.add(entry.record.deploymentId);
2945
+ }
2672
2946
  return {
2673
2947
  recordPath: live.record.recordPath,
2674
2948
  generationRoot: live.generationRoot,
2675
- identity: live.record.identity,
2949
+ identity: {
2950
+ ...live.record.identity,
2951
+ ...(Object.keys(retainedDeployments).length === 0 ? {} : { retainedDeployments }),
2952
+ ...(Object.keys(pendingRetirements).length === 0 ? {} : { pendingRetirements }),
2953
+ },
2954
+ protectedDeploymentIds: [...protectedDeploymentIds],
2676
2955
  status: live.record.status,
2677
2956
  deploymentIds: [...deploymentIds],
2678
2957
  worldCloudflare: live.record.worldCloudflare,
@@ -2683,6 +2962,19 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2683
2962
  const base = await parseEveDestroyRecord(recordPath, environment, name);
2684
2963
  if (base === undefined)
2685
2964
  return undefined;
2965
+ let identity = base.identity;
2966
+ try {
2967
+ const completed = JSON.parse(await readFile(join(dirname(recordPath), "completed-retirements.json"), "utf8"));
2968
+ if (Array.isArray(completed) && completed.every((id) => typeof id === "string")) {
2969
+ const pendingRetirements = { ...identity.pendingRetirements };
2970
+ for (const id of completed)
2971
+ delete pendingRetirements[id];
2972
+ identity = { ...identity, pendingRetirements };
2973
+ }
2974
+ }
2975
+ catch {
2976
+ // Missing or unreadable cleanup evidence must not suppress a retry.
2977
+ }
2686
2978
  let parsed;
2687
2979
  try {
2688
2980
  parsed = JSON.parse(await readFile(recordPath, "utf8"));
@@ -2726,7 +3018,7 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2726
3018
  Array.isArray(rawMigrations);
2727
3019
  const invalidHistory = () => ({
2728
3020
  recordPath,
2729
- identity: base.identity,
3021
+ identity,
2730
3022
  status,
2731
3023
  deploymentId,
2732
3024
  worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
@@ -2753,7 +3045,7 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2753
3045
  }
2754
3046
  return {
2755
3047
  recordPath,
2756
- identity: base.identity,
3048
+ identity,
2757
3049
  status,
2758
3050
  deploymentId,
2759
3051
  worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
@@ -2775,7 +3067,7 @@ async function defaultDestroyTargetRead(request) {
2775
3067
  "images",
2776
3068
  "list",
2777
3069
  "--json",
2778
- ]);
3070
+ ], WRANGLER_OPERATION_TIMEOUT_MS);
2779
3071
  const tagsByRepository = registryTagsByRepository(images.value);
2780
3072
  if (!images.failed && tagsByRepository !== undefined) {
2781
3073
  const present = [];
@@ -2854,12 +3146,13 @@ async function defaultContainerDelete(request) {
2854
3146
  * repository is deleted individually.
2855
3147
  */
2856
3148
  async function defaultRegistryRepositoryDelete(request) {
3149
+ // Catalog reads include Cloudflare snapshot tags and can exceed 30 seconds.
2857
3150
  const images = await readWranglerJson([
2858
3151
  "containers",
2859
3152
  "images",
2860
3153
  "list",
2861
3154
  "--json",
2862
- ]);
3155
+ ], WRANGLER_OPERATION_TIMEOUT_MS);
2863
3156
  const collection = jsonCollection(images.value);
2864
3157
  if (images.failed || collection === undefined)
2865
3158
  return "indeterminate";
@@ -2885,9 +3178,11 @@ async function defaultRegistryRepositoryDelete(request) {
2885
3178
  deletedAny = true;
2886
3179
  continue;
2887
3180
  }
2888
- if (!/(?:does not exist|not found|404)/iu.test(result.stderr)) {
2889
- return "indeterminate";
2890
- }
3181
+ }
3182
+ const after = await readWranglerJson(["containers", "images", "list", "--json"], WRANGLER_OPERATION_TIMEOUT_MS);
3183
+ const remaining = after.failed ? undefined : registryTagsByRepository(after.value);
3184
+ if (remaining === undefined || (remaining.get(request.repository)?.length ?? 0) > 0) {
3185
+ return "indeterminate";
2891
3186
  }
2892
3187
  return deletedAny ? "deleted" : "absent";
2893
3188
  }
@@ -2905,13 +3200,13 @@ function ownedRegistryRepository(identity, environment, name, accountId) {
2905
3200
  identity.accountId !== accountId) {
2906
3201
  return undefined;
2907
3202
  }
2908
- const repository = `eden-eve-${stableDeploymentKey(identity.projectId, identity.accountId, environment, name)}-${identity.generationId}`;
2909
- const expected = `registry.cloudflare.com/${identity.accountId}/${repository}@`;
2910
- if (!identity.containerImage.startsWith(expected) ||
2911
- imageDigestFromReference(identity.containerImage) === undefined) {
3203
+ const prefix = `eden-eve-${stableDeploymentKey(identity.projectId, identity.accountId, environment, name)}-`;
3204
+ const suffixes = [identity.generationId];
3205
+ if (identity.retainedDeployments !== undefined && identity.deploymentId !== undefined)
3206
+ suffixes.push(identity.deploymentId);
3207
+ if (imageDigestFromReference(identity.containerImage) === undefined)
2912
3208
  return undefined;
2913
- }
2914
- return repository;
3209
+ return suffixes.map((suffix) => `${prefix}${suffix}`).find((repository) => identity.containerImage.startsWith(`registry.cloudflare.com/${identity.accountId}/${repository}@`));
2915
3210
  }
2916
3211
  /**
2917
3212
  * The registry repository an indeterminate deploy attempt provably pushed.
@@ -2964,17 +3259,31 @@ async function collectEveDestroyRegistryRepositories(projectRoot, environment, n
2964
3259
  const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
2965
3260
  const entries = await readdir(generationsRoot).catch(() => []);
2966
3261
  const repositories = new Set();
3262
+ const addRetained = (identity) => {
3263
+ for (const [id, retained] of Object.entries({
3264
+ ...identity.retainedDeployments, ...identity.pendingRetirements,
3265
+ })) {
3266
+ const repository = ownedRegistryRepository({
3267
+ ...identity, deploymentId: id, containerImage: retained.image, generationId: retained.generationId,
3268
+ }, environment, name, record.accountId);
3269
+ if (repository !== undefined)
3270
+ repositories.add(repository);
3271
+ }
3272
+ };
3273
+ addRetained(record);
2967
3274
  const targetKey = record.projectId === undefined
2968
3275
  ? undefined
2969
3276
  : stableDeploymentKey(record.projectId, record.accountId, environment, name);
2970
3277
  for (const entry of entries) {
2971
3278
  const generationRoot = join(generationsRoot, entry);
2972
- const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
3279
+ const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
2973
3280
  const deployedRepository = parsed === undefined
2974
3281
  ? undefined
2975
3282
  : ownedRegistryRepository(parsed.identity, environment, name, record.accountId);
2976
3283
  if (deployedRepository !== undefined)
2977
3284
  repositories.add(deployedRepository);
3285
+ if (parsed !== undefined)
3286
+ addRetained(parsed.identity);
2978
3287
  if (targetKey === undefined)
2979
3288
  continue;
2980
3289
  const attemptRepository = await attemptRegistryRepository(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
@@ -1,4 +1,4 @@
1
- export declare const EVE_RESERVED_HOST_VARIABLES: readonly ["HOST", "NITRO_HOST", "PORT", "NITRO_PORT", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "WORKFLOW_LOCAL_BASE_URL", "EDEN_WORLD_URL", "EDEN_EVE_DEPLOYMENT_ID", "EDEN_EVE_GENERATION_ID"];
1
+ export declare const EVE_RESERVED_HOST_VARIABLES: readonly ["HOST", "NITRO_HOST", "PORT", "NITRO_PORT", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "WORKFLOW_LOCAL_BASE_URL", "EDEN_WORLD_URL", "EDEN_WORLD_TOKEN", "EDEN_EVE_WORLD_RPC_AUTH", "EDEN_EVE_DEPLOYMENT_ID", "EDEN_EVE_GENERATION_ID", "EDEN_EVE_RELEASE_ID", "EDEN_EVE_DEPLOYMENTS", "EDEN_AFFINITY_CONTRACT", "EVE_SCHEDULES_DISABLED", "EDEN_EVE_CONTROL_TOKEN"];
2
2
  export type EveReservedHostVariable = (typeof EVE_RESERVED_HOST_VARIABLES)[number];
3
3
  export declare const EVE_START_COMMAND: readonly ["./node_modules/.bin/eve", "start", "--host", "0.0.0.0", "--port", "8080"];
4
4
  export declare const EVE_HOST_ENVIRONMENT: Readonly<{
@@ -10,8 +10,15 @@ export const EVE_RESERVED_HOST_VARIABLES = Object.freeze([
10
10
  "NODE_EXTRA_CA_CERTS",
11
11
  "WORKFLOW_LOCAL_BASE_URL",
12
12
  "EDEN_WORLD_URL",
13
+ "EDEN_WORLD_TOKEN",
14
+ "EDEN_EVE_WORLD_RPC_AUTH",
13
15
  "EDEN_EVE_DEPLOYMENT_ID",
14
16
  "EDEN_EVE_GENERATION_ID",
17
+ "EDEN_EVE_RELEASE_ID",
18
+ "EDEN_EVE_DEPLOYMENTS",
19
+ "EDEN_AFFINITY_CONTRACT",
20
+ "EVE_SCHEDULES_DISABLED",
21
+ "EDEN_EVE_CONTROL_TOKEN",
15
22
  ]);
16
23
  export const EVE_START_COMMAND = Object.freeze([
17
24
  "./node_modules/.bin/eve",
@@ -458,6 +458,7 @@ COPY node_modules /candidate/node_modules
458
458
  ${sandboxContextCopy}${sourceEntries.map((entry) => `COPY ${JSON.stringify([entry, `/candidate/${entry}`])}`).join("\n")}
459
459
 
460
460
  FROM --platform=linux/amd64 ${imageReference(nodeImage)} AS runtime
461
+ RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*
461
462
  WORKDIR /workspace
462
463
  ENV HOST=0.0.0.0 \\
463
464
  NITRO_HOST=0.0.0.0 \\
@@ -582,6 +583,8 @@ function runtimeContainerEnvironment(request) {
582
583
  ...(request.workflowWorld === "@moinulmoin/eden-world-cloudflare"
583
584
  ? {
584
585
  EDEN_WORLD_URL: `${request.publicOrigin ?? "http://localhost:8080"}/__eden/world/rpc`,
586
+ EDEN_EVE_DEPLOYMENT_ID: `eden-smoke-${request.candidate.generationId}`,
587
+ EDEN_EVE_RELEASE_ID: `eden-smoke-${request.candidate.generationId}`,
585
588
  CBOR_NATIVE_ACCELERATION_DISABLED: "true",
586
589
  }
587
590
  : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moinulmoin/eden",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Deploy existing Eve projects to your own Cloudflare account.",
5
5
  "keywords": [
6
6
  "ai",
@@ -31,7 +31,7 @@
31
31
  "NOTICE"
32
32
  ],
33
33
  "dependencies": {
34
- "@moinulmoin/eden-runtime-cloudflare": "0.9.0",
34
+ "@moinulmoin/eden-runtime-cloudflare": "0.10.0",
35
35
  "wrangler": "4.147.0"
36
36
  },
37
37
  "devDependencies": {