@wtfalch/foundry 0.17.1 → 0.18.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.
@@ -1,14 +1,16 @@
1
+ import { tunnelIngressHostnames } from './hostnames.js';
2
+ import { lockedHostnames } from './locked-hostnames.js';
1
3
  import { appStepKey } from './platform-diff.js';
2
4
  /**
3
- * F7 (#317): one names key per entry with `uses.names`, minted directly
4
- * below the org with the names service (`NamesPort.mintKey({
5
+ * F7 (#317): one edge key per entry with `uses.edge`, minted directly
6
+ * below the org with the edge service (`EdgePort.mintKey({
5
7
  * organisationId, ... })` -- no org-level root key the way files/ai have
6
- * one, since the names service takes the org id itself). `NAMES_URL` and
7
- * `NAMES_API_KEY` go to the entry's env and nowhere else. A names key has no
8
+ * one, since the edge service takes the org id itself). `EDGE_URL` and
9
+ * `EDGE_API_KEY` go to the entry's env and nowhere else. A edge key has no
8
10
  * declared renewal window in the issue, so unlike `planEntryAiKeys`/
9
11
  * `planFilesKeys` this mints once and never renews.
10
12
  *
11
- * `NAMES_*` is written in this step, after the app's own `coolify:app`
13
+ * `EDGE_*` is written in this step, after the app's own `coolify:app`
12
14
  * create (which never knows the key yet), so the app's first boot has
13
15
  * neither and its first deploy restarts once this step's env write lands --
14
16
  * the same one-extra-restart shape `planEntryAiKeys`'s `AI_*`/
@@ -17,55 +19,77 @@ import { appStepKey } from './platform-diff.js';
17
19
  * A domain change after mint is not detected (#317 re-review): the key is
18
20
  * bound to the domain passed at mint time, but nothing here records that
19
21
  * domain to compare against a later `org.domain`, so an org that changes
20
- * domains keeps a names key still bound to the old one. Not built --
21
- * resolving it needs a manual revoke-and-re-mint (drop `uses.names`, apply,
22
+ * domains keeps a edge key still bound to the old one. Not built --
23
+ * resolving it needs a manual revoke-and-re-mint (drop `uses.edge`, apply,
22
24
  * re-add it) until this is.
23
25
  */
24
- export const entryNamesUndeliveredScopeKey = (entryKey) => `names_key_undelivered_${entryKey}`;
25
- /** The id of a key that was live when its entry stopped declaring `uses.names`, once handled. */
26
- export const entryNamesRetiredScopeKey = (entryKey) => `names_key_retired_${entryKey}`;
27
- export const ENTRY_NAMES_KEY_DELIVERY_ATTEMPTS = 3;
28
- /** How long an entry's names key lives. The issue names no renewal window, so this mints once and never renews. */
29
- export const NAMES_ENTRY_KEY_DAYS = 365;
26
+ export const entryEdgeUndeliveredScopeKey = (entryKey) => `edge_key_undelivered_${entryKey}`;
27
+ /** The id of a key that was live when its entry stopped declaring `uses.edge`, once handled. */
28
+ export const entryEdgeRetiredScopeKey = (entryKey) => `edge_key_retired_${entryKey}`;
29
+ export const ENTRY_EDGE_KEY_DELIVERY_ATTEMPTS = 3;
30
+ /** How long an entry's edge key lives. The issue names no renewal window, so this mints once and never renews. */
31
+ export const EDGE_ENTRY_KEY_DAYS = 365;
30
32
  /** The ISO time a delivered key's mint succeeded -- the only trace of its age, since there is no renewal to track against. */
31
- export const entryNamesMintedAtScopeKey = (entryKey) => `names_key_minted_at_${entryKey}`;
33
+ export const entryEdgeMintedAtScopeKey = (entryKey) => `edge_key_minted_at_${entryKey}`;
32
34
  /** A key with fewer days left than this is surfaced as a blocked warning row (#317 review): still live, but nothing here renews it. */
33
- export const NAMES_ENTRY_KEY_WARN_WINDOW_DAYS = 30;
35
+ export const EDGE_ENTRY_KEY_WARN_WINDOW_DAYS = 30;
34
36
  const DAY_MS = 24 * 60 * 60 * 1000;
35
- /** The reserved labels last sent to the names service, as that sorted list's own JSON. */
36
- export const NAMES_RESERVED_LABELS_APPLIED_SCOPE_KEY = 'names_reserved_labels_applied';
37
- export const entryNamesKeyName = (org, entry) => `${org.slug}-${entry.key}-names`;
37
+ /** The reserved labels last sent to the edge service, as that sorted list's own JSON. */
38
+ export const EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY = 'edge_reserved_labels_applied';
39
+ export const entryEdgeKeyName = (org, entry) => `${org.slug}-${entry.key}-edge`;
38
40
  /**
39
- * Every label a declared entry serves (`@`/`*` reserve nothing of their own:
40
- * the bare domain and the wildcard are not names a customer could claim
41
- * under names anyway), plus `www`, which `setReservedLabels`'s own doc
42
- * comment in `executor.ts` explains is reserved unconditionally.
41
+ * Mail names foundry's mail setup puts on the org's domain. Stalwart's and
42
+ * Lettermint's own record names (DKIM selectors, return path) come back from
43
+ * the mail port at apply time, so only the fixed ones are listed here.
43
44
  */
44
- export function namesReservedLabels(org) {
45
+ const MAIL_LABELS = ['mail', 'autoconfig', 'autodiscover', '_dmarc'];
46
+ /**
47
+ * Every label under `org.domain` that foundry provisions or the platform
48
+ * uses, so edge never lets a company claim one: the hostnames foundry routes
49
+ * (`tunnelIngressHostnames`: entries, site, cms, calls, Agora, Boule, share),
50
+ * the names it locks (`lockedHostnames`: also the sign-in host and the
51
+ * platform's service hostnames), each entry's own labels, the mail labels
52
+ * and `www`. The entry's `*` is the space users claim in, so it reserves
53
+ * nothing, and neither does the bare domain.
54
+ *
55
+ * An org without a domain has no hostnames edge sees (`uses.edge` needs a
56
+ * domain), so it reserves `www` only.
57
+ */
58
+ export function edgeReservedLabels(org, platformHostnames = []) {
45
59
  const labels = new Set(['www']);
60
+ const { domain } = org;
61
+ if (!domain)
62
+ return [...labels];
63
+ for (const label of MAIL_LABELS)
64
+ labels.add(label);
46
65
  for (const entry of org.hosting.entries) {
47
66
  for (const name of entry.hostnames) {
48
67
  if (name !== '@' && name !== '*')
49
68
  labels.add(name);
50
69
  }
51
70
  }
71
+ const suffix = `.${domain}`;
72
+ for (const host of [...tunnelIngressHostnames(org), ...lockedHostnames(org, platformHostnames)]) {
73
+ if (host.endsWith(suffix) && !host.startsWith('*.'))
74
+ labels.add(host.slice(0, -suffix.length));
75
+ }
52
76
  return [...labels].sort();
53
77
  }
54
78
  /**
55
- * One step per entry whose names key is missing, plus one retire step per
56
- * entry whose `uses.names` was removed but still has a recorded key.
57
- * Blocked, not planned (rule 2), while the platform has no names service
79
+ * One step per entry whose edge key is missing, plus one retire step per
80
+ * entry whose `uses.edge` was removed but still has a recorded key.
81
+ * Blocked, not planned (rule 2), while the platform has no edge service
58
82
  * wired, the entry's app is not recorded, or a previous mint's delivery
59
83
  * failed.
60
84
  */
61
- export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, now = Date.now()) {
85
+ export function planEntryEdgeKeys(org, recordedIds, plannedSteps, readiness, now = Date.now()) {
62
86
  const steps = [];
63
87
  const blocked = [];
64
88
  const planned = (system, kind, k) => plannedSteps.some((p) => p.action.system === system && p.action.kind === kind && p.action.key === k);
65
- // A key whose entry lost `uses.names`, or whose entry is gone entirely --
89
+ // A key whose entry lost `uses.edge`, or whose entry is gone entirely --
66
90
  // either way it is no longer in `declared`, the same "gone" scan
67
91
  // `planEntryAiKeys`/`planFilesKeys` run over their own recorded keys.
68
- // Gated on `namesReady`: without a port there is nothing that could revoke
92
+ // Gated on `edgeReady`: without a port there is nothing that could revoke
69
93
  // it, so it is reported blocked, not planned to a step that would throw.
70
94
  //
71
95
  // Scanned from both the main recorded id and the undelivered marker: a
@@ -73,9 +97,9 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
73
97
  // throws before `runSteps` sees a `resourceId`), so scanning the main
74
98
  // prefix alone would never find it to revoke once its entry is gone
75
99
  // (#317 review).
76
- const declared = new Set(org.hosting.entries.filter((e) => e.uses?.names).map((e) => e.key));
77
- const mainPrefix = `names:key:${org.id}:`;
78
- const undeliveredPrefix = 'names_key_undelivered_';
100
+ const declared = new Set(org.hosting.entries.filter((e) => e.uses?.edge).map((e) => e.key));
101
+ const mainPrefix = `edge:key:${org.id}:`;
102
+ const undeliveredPrefix = 'edge_key_undelivered_';
79
103
  const goneEntryKeys = new Set();
80
104
  for (const [name, id] of Object.entries(recordedIds)) {
81
105
  if (!id)
@@ -95,39 +119,39 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
95
119
  // (mint, three failed deliveries, undeclare again) must not hide
96
120
  // behind id A's retired marker just because id A's main-id field is
97
121
  // still truthy (#317 re-review).
98
- if (name.startsWith(mainPrefix) && recordedIds[entryNamesRetiredScopeKey(entryKey)] === id) {
122
+ if (name.startsWith(mainPrefix) && recordedIds[entryEdgeRetiredScopeKey(entryKey)] === id) {
99
123
  continue;
100
124
  }
101
125
  goneEntryKeys.add(entryKey);
102
126
  }
103
127
  for (const entryKey of goneEntryKeys) {
104
- if (!readiness.namesReady) {
128
+ if (!readiness.edgeReady) {
105
129
  blocked.push({
106
- description: `${org.slug}'s ${entryKey} names key is blocked, not planned: the host supplies no names service, or the platform has not enabled it`,
130
+ description: `${org.slug}'s ${entryKey} edge key is blocked, not planned: the host supplies no edge service, or the platform has not enabled it`,
107
131
  key: `${mainPrefix}${entryKey}`,
108
132
  });
109
133
  continue;
110
134
  }
111
135
  steps.push({
112
136
  action: {
113
- system: 'names',
137
+ system: 'edge',
114
138
  kind: 'key',
115
139
  key: `${org.id}:${entryKey}`,
116
140
  desired: { mode: 'retire' },
117
141
  },
118
142
  step: {
119
- description: `revoke ${org.slug}'s ${entryKey} names key: the entry no longer declares uses.names`,
143
+ description: `revoke ${org.slug}'s ${entryKey} edge key: the entry no longer declares uses.edge`,
120
144
  destroys: true,
121
145
  migrates: false,
122
146
  },
123
147
  });
124
148
  }
125
149
  for (const entry of org.hosting.entries) {
126
- if (!entry.uses?.names)
150
+ if (!entry.uses?.edge)
127
151
  continue;
128
152
  const stepKey = appStepKey(org, entry.key);
129
- const key = `names:key:${stepKey}`;
130
- const liveKeyId = recordedIds[key] && recordedIds[entryNamesRetiredScopeKey(entry.key)] !== recordedIds[key]
153
+ const key = `edge:key:${stepKey}`;
154
+ const liveKeyId = recordedIds[key] && recordedIds[entryEdgeRetiredScopeKey(entry.key)] !== recordedIds[key]
131
155
  ? recordedIds[key]
132
156
  : undefined;
133
157
  if (liveKeyId) {
@@ -140,20 +164,20 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
140
164
  // mint could ever clear) can tell the two apart -- a permanently
141
165
  // "blocked" row past day 335 that no action removes is the problem
142
166
  // this prefix exists to avoid.
143
- const mintedAt = recordedIds[entryNamesMintedAtScopeKey(entry.key)];
167
+ const mintedAt = recordedIds[entryEdgeMintedAtScopeKey(entry.key)];
144
168
  if (mintedAt) {
145
- const expiresAt = Date.parse(mintedAt) + NAMES_ENTRY_KEY_DAYS * DAY_MS;
169
+ const expiresAt = Date.parse(mintedAt) + EDGE_ENTRY_KEY_DAYS * DAY_MS;
146
170
  const daysLeft = Math.floor((expiresAt - now) / DAY_MS);
147
- if (daysLeft < NAMES_ENTRY_KEY_WARN_WINDOW_DAYS) {
171
+ if (daysLeft < EDGE_ENTRY_KEY_WARN_WINDOW_DAYS) {
148
172
  blocked.push({
149
- description: `warning: ${org.slug}'s ${entry.key} names key ${daysLeft < 0 ? 'expired' : 'expires'} ${new Date(expiresAt).toISOString().slice(0, 10)} and is never renewed (#317): mint a new key by hand and update the entry's env`,
173
+ description: `warning: ${org.slug}'s ${entry.key} edge key ${daysLeft < 0 ? 'expired' : 'expires'} ${new Date(expiresAt).toISOString().slice(0, 10)} and is never renewed (#317): mint a new key by hand and update the entry's env`,
150
174
  key,
151
175
  });
152
176
  }
153
177
  }
154
178
  continue;
155
179
  }
156
- const at = `${org.slug}'s ${entry.key} names key`;
180
+ const at = `${org.slug}'s ${entry.key} edge key`;
157
181
  const block = (why) => blocked.push({ description: `${at} is blocked: ${why}`, key });
158
182
  // An undelivered key from a previous mint is no longer a reason to
159
183
  // block forever: nothing here keeps the secret around to retry
@@ -162,8 +186,8 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
162
186
  // re-review) -- blocking indefinitely, as a prior round did, left the
163
187
  // orphan live for up to 365 days with no recorded trace an operator
164
188
  // could act on beyond this description.
165
- if (!readiness.namesReady) {
166
- block('the host supplies no names service, or the platform has not enabled it');
189
+ if (!readiness.edgeReady) {
190
+ block('the host supplies no edge service, or the platform has not enabled it');
167
191
  continue;
168
192
  }
169
193
  if (!recordedIds[`coolify:app:${stepKey}`] && !planned('coolify', 'app', stepKey)) {
@@ -172,17 +196,17 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
172
196
  }
173
197
  steps.push({
174
198
  action: {
175
- system: 'names',
199
+ system: 'edge',
176
200
  kind: 'key',
177
201
  key: stepKey,
178
202
  desired: {
179
203
  mode: 'mint',
180
- name: entryNamesKeyName(org, entry),
181
- namesUrl: readiness.namesUrl,
204
+ name: entryEdgeKeyName(org, entry),
205
+ edgeUrl: readiness.edgeUrl,
182
206
  },
183
207
  },
184
208
  step: {
185
- description: `mint ${at} below the org, write NAMES_* env, restart the app`,
209
+ description: `mint ${at} below the org, write EDGE_* env, restart the app`,
186
210
  destroys: false,
187
211
  migrates: false,
188
212
  },
@@ -192,41 +216,41 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
192
216
  }
193
217
  /**
194
218
  * The reserved-labels step: resent whenever the org's own label set
195
- * (`namesReservedLabels`) differs from what was last sent. While the org
196
- * uses names at all (at least one entry with `uses.names`) that set is
197
- * `namesReservedLabels(org)`; once the last entry drops `uses.names`, this
219
+ * (`edgeReservedLabels`) differs from what was last sent. While the org
220
+ * uses edge at all (at least one entry with `uses.edge`) that set is
221
+ * `edgeReservedLabels(org)`; once the last entry drops `uses.edge`, this
198
222
  * clears whatever was last sent (an empty list) rather than leaving it
199
223
  * stuck in names forever -- a one-time step, same as any other change.
200
224
  */
201
- export function planNamesReservedLabels(org, recordedIds, readiness) {
202
- const usesNames = org.hosting.entries.some((e) => e.uses?.names);
203
- const applied = recordedIds[NAMES_RESERVED_LABELS_APPLIED_SCOPE_KEY];
204
- if (!usesNames && (!applied || applied === '[]')) {
225
+ export function planEdgeReservedLabels(org, recordedIds, readiness, platformHostnames = []) {
226
+ const usesEdge = org.hosting.entries.some((e) => e.uses?.edge);
227
+ const applied = recordedIds[EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY];
228
+ if (!usesEdge && (!applied || applied === '[]')) {
205
229
  return { step: null, blocked: [] };
206
230
  }
207
- if (!readiness.namesReady) {
231
+ if (!readiness.edgeReady) {
208
232
  return {
209
233
  step: null,
210
234
  blocked: [
211
235
  {
212
- description: `${org.slug}'s reserved labels are blocked: the host supplies no names service, or the platform has not enabled it`,
213
- key: `names:reserved-labels:${org.id}`,
236
+ description: `${org.slug}'s reserved labels are blocked: the host supplies no edge service, or the platform has not enabled it`,
237
+ key: `edge:reserved-labels:${org.id}`,
214
238
  },
215
239
  ],
216
240
  };
217
241
  }
218
- const labels = usesNames ? namesReservedLabels(org) : [];
242
+ const labels = usesEdge ? edgeReservedLabels(org, platformHostnames) : [];
219
243
  const wantJson = JSON.stringify(labels);
220
244
  if (applied === wantJson) {
221
245
  return { step: null, blocked: [] };
222
246
  }
223
247
  return {
224
248
  step: {
225
- action: { system: 'names', kind: 'reserved-labels', key: org.id, desired: { labels } },
249
+ action: { system: 'edge', kind: 'reserved-labels', key: org.id, desired: { labels } },
226
250
  step: {
227
- description: usesNames
228
- ? `send ${org.slug}'s reserved labels to names: ${labels.join(', ')}`
229
- : `clear ${org.slug}'s reserved labels from names: no entry uses names any more`,
251
+ description: usesEdge
252
+ ? `send ${org.slug}'s reserved labels to edge: ${labels.join(', ')}`
253
+ : `clear ${org.slug}'s reserved labels from edge: no entry uses edge any more`,
230
254
  destroys: false,
231
255
  migrates: false,
232
256
  },
@@ -565,11 +565,11 @@ export interface WorkersRateLimitBinding {
565
565
  }
566
566
  /**
567
567
  * The Cloudflare Workers calls `cloudflareWorker` makes (#318, F8): deploys
568
- * a plain Worker from an entry's `edge` declaration and routes it on the
568
+ * a plain Worker from an entry's `worker` declaration and routes it on the
569
569
  * org's own zone. Optional on {@link FoundryStepExecutorPorts} -- a host
570
570
  * with no Workers rights on its Cloudflare token (the issue's own "Human
571
571
  * steps": the token needs them added first) wires none, and the planner
572
- * reports every `edge` entry blocked instead of planning a step that would
572
+ * reports every `worker` entry blocked instead of planning a step that would
573
573
  * throw (`FoundryPlanApplyOptions.edgeWorkers`).
574
574
  *
575
575
  * `uploadScript` takes the *declared artifact reference*, not bytes:
@@ -604,10 +604,13 @@ export interface WorkersPort {
604
604
  /** The script name currently bound to `pattern` in `zoneId`, or `undefined` for no route there. */
605
605
  findRoute(zoneId: string, pattern: string): Promise<{
606
606
  id: string;
607
- script: string;
607
+ script?: string;
608
608
  } | undefined>;
609
- /** Creates the route if absent; `cloudflareWorker` only calls this once `findRoute` has confirmed the pattern is foundry's own or unclaimed. */
610
- createRoute(zoneId: string, pattern: string, script: string): Promise<{
609
+ /**
610
+ * Creates the route if absent; `cloudflareWorker` only calls this once `findRoute` has confirmed the pattern is foundry's own or unclaimed.
611
+ * With no `script` it creates Cloudflare's "no Worker" route (`POST /zones/{zone_id}/workers/routes` with `pattern` only), which turns Workers off for that pattern; a more specific pattern wins over a wildcard.
612
+ */
613
+ createRoute(zoneId: string, pattern: string, script?: string): Promise<{
611
614
  id: string;
612
615
  }>;
613
616
  /** A no-op, not a throw, when `routeId` is already gone. */
@@ -1338,22 +1341,22 @@ export interface AiOrganisationPort {
1338
1341
  }): Promise<unknown>;
1339
1342
  }
1340
1343
  /**
1341
- * #317 (F7): the names service's own management API
1342
- * (https://github.com/wtfalch/names, live on main, offering `mintKey`,
1344
+ * #317 (F7): the edge service's own management API
1345
+ * (https://github.com/wtfalch/edge, live on main, offering `mintKey`,
1343
1346
  * `revokeKey` and `setReservedLabels`) -- this port states the shape this
1344
1347
  * executor assumes of it. `organisationId` is the org itself: unlike
1345
- * `AiOrganisationPort.mintNestedKey`, names takes no separate minter key
1346
- * id, so `namesEntryKey` mints directly below the org. A real adapter
1348
+ * `AiOrganisationPort.mintNestedKey`, edge takes no separate minter key
1349
+ * id, so `edgeEntryKey` mints directly below the org. A real adapter
1347
1350
  * wiring this port into `platform-providers.ts` is tracked separately
1348
1351
  * (https://github.com/wtfalch/archon/issues/339).
1349
1352
  */
1350
- export interface NamesPort {
1353
+ export interface EdgePort {
1351
1354
  /**
1352
- * `domain` is required: names's own `mintKey` refuses a request without a
1355
+ * `domain` is required: edge's own `mintKey` refuses a request without a
1353
1356
  * lowercase hostname and binds the key to it
1354
- * (`wtfalch/names`'s `packages/service/src/keys.ts`) -- `namesEntryKey`
1355
- * passes `org.domain`, which `validateEntryNames` already requires before
1356
- * `uses.names` is accepted.
1357
+ * (`wtfalch/edge`'s `packages/service/src/keys.ts`) -- `edgeEntryKey`
1358
+ * passes `org.domain`, which `validateEntryEdge` already requires before
1359
+ * `uses.edge` is accepted.
1357
1360
  */
1358
1361
  mintKey(input: {
1359
1362
  organisationId: string;
@@ -1437,14 +1440,14 @@ export interface FoundryStepExecutorPorts {
1437
1440
  filesOrganisation?: FilesOrganisationPort;
1438
1441
  aiOrganisation?: AiOrganisationPort;
1439
1442
  /**
1440
- * #317 (F7): mints an entry's names key -- see {@link NamesPort}'s own
1443
+ * #317 (F7): mints an entry's edge key -- see {@link EdgePort}'s own
1441
1444
  * doc comment. Optional the same way `jobsScheduler` is: a host with no
1442
- * names service deployed builds the executor without one, and tells the
1443
- * planner so (`FoundryPlanApplyOptions.names`), which then reports each
1444
- * entry's `uses.names` step as blocked instead of planning one that would
1445
+ * edge service deployed builds the executor without one, and tells the
1446
+ * planner so (`FoundryPlanApplyOptions.edge`), which then reports each
1447
+ * entry's `uses.edge` step as blocked instead of planning one that would
1445
1448
  * throw.
1446
1449
  */
1447
- names?: NamesPort;
1450
+ edge?: EdgePort;
1448
1451
  /**
1449
1452
  * #177: builds the files, AI and jobs ports from the platform's addresses
1450
1453
  * and held keys. A port set above overrides it (tests); with neither, the
@@ -1468,9 +1471,9 @@ export interface FoundryStepExecutorPorts {
1468
1471
  */
1469
1472
  broker?: BrokerPort;
1470
1473
  /**
1471
- * #318: deploys and routes an entry's `edge` Worker -- see
1474
+ * #318: deploys and routes an entry's `worker` Worker -- see
1472
1475
  * {@link WorkersPort}'s own doc comment. Optional the same way `broker` is:
1473
- * without it the plan reports each `edge` entry blocked (the host also
1476
+ * without it the plan reports each `worker` entry blocked (the host also
1474
1477
  * sets `FoundryPlanApplyOptions.edgeWorkers`).
1475
1478
  */
1476
1479
  workers?: WorkersPort;
@@ -1618,10 +1621,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1618
1621
  */
1619
1622
  private cloudflareRateLimit;
1620
1623
  /**
1621
- * `plan/diff.ts`'s `planEdgeWorkers` (#318, F8): deploys one entry's
1622
- * `edge` Worker and routes it on the org's own zone. Throws by name
1624
+ * `plan/diff.ts`'s `planWorkers` (#318, F8): deploys one entry's
1625
+ * `worker` Worker and routes it on the org's own zone. Throws by name
1623
1626
  * without a {@link WorkersPort}, the same `jobsRegistration` shape --
1624
- * `planEdgeWorkers` reports the step blocked instead while the host has
1627
+ * `planWorkers` reports the step blocked instead while the host has
1625
1628
  * none, so this only fires against a stale plan. The route is checked
1626
1629
  * before upload: a foreign route already on the pattern is refused, never
1627
1630
  * adopted or replaced (`cloudflareAccess`'s own precedent for a foreign
@@ -1638,6 +1641,14 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1638
1641
  * live state actually matches it.
1639
1642
  */
1640
1643
  private cloudflareWorker;
1644
+ /**
1645
+ * Makes the no-Worker routes recorded for an entry match `patterns`: deletes
1646
+ * the ones no longer wanted (all of them if the zone changed), creates the
1647
+ * missing ones without a script. A route already on a pattern that is bound
1648
+ * to a Worker is foreign and refused; one with no script is adopted. Records
1649
+ * and persists after each change.
1650
+ */
1651
+ private syncWorkerExclusions;
1641
1652
  /**
1642
1653
  * The zone id a recorded route lives in, or `undefined` only when that
1643
1654
  * zone is *positively* gone -- its apex resolves to no zone, or to a
@@ -1651,15 +1662,15 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1651
1662
  * live on the old one, the exact bug a recorded-per-route zone exists to
1652
1663
  * avoid (#318 re-review).
1653
1664
  */
1654
- private liveEdgeZone;
1665
+ private liveWorkerZone;
1655
1666
  /** Deletes the route an interrupted apply was creating, if `findRoute` shows it exists, points at that apply's script, and is not `keepRouteId`. */
1656
- private deletePendingEdgeRoute;
1667
+ private deletePendingWorkerRoute;
1657
1668
  /**
1658
1669
  * Retire and destroy share this: delete the recorded route on its own
1659
1670
  * zone, then any route an interrupted apply created but never recorded,
1660
1671
  * then the script. Records nothing -- the caller does.
1661
1672
  */
1662
- private teardownEdgeWorker;
1673
+ private teardownWorker;
1663
1674
  private zitadelIssuer;
1664
1675
  /**
1665
1676
  * `plan/diff.ts`'s `planOwner`: invites the org's owner into its ZITADEL
@@ -2716,21 +2727,21 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2716
2727
  */
2717
2728
  private revokeEntryKey;
2718
2729
  /**
2719
- * F7 (#317): mints one entry's names key directly below the org (names
2720
- * takes no separate minter key id, unlike ai/files), writes `NAMES_URL`
2721
- * and `NAMES_API_KEY` to the entry's app env, then restarts it. Retires
2722
- * the key when `uses.names` is removed. Follows the "one mint" rule
2730
+ * F7 (#317): mints one entry's edge key directly below the org (edge
2731
+ * takes no separate minter key id, unlike ai/files), writes `EDGE_URL`
2732
+ * and `EDGE_API_KEY` to the entry's app env, then restarts it. Retires
2733
+ * the key when `uses.edge` is removed. Follows the "one mint" rule
2723
2734
  * (CLAUDE.md "Platform steps"): the id is recorded as undelivered and
2724
2735
  * persisted before delivery is attempted, so a crash or a failed write
2725
- * never leads to a second mint -- `planEntryNamesKeys` blocks instead.
2736
+ * never leads to a second mint -- `planEntryEdgeKeys` blocks instead.
2726
2737
  */
2727
- private namesEntryKey;
2738
+ private edgeEntryKey;
2728
2739
  /**
2729
- * F7 (#317): `plan/entry-names-key.ts`'s `planNamesReservedLabels` --
2730
- * tells the names service every label this org's own entries already
2731
- * hold, so names refuses a customer claiming one of them.
2740
+ * F7 (#317): `plan/entry-edge-key.ts`'s `planEdgeReservedLabels` --
2741
+ * tells the edge service every label this org's own entries already
2742
+ * hold, so edge refuses a customer claiming one of them.
2732
2743
  */
2733
- private namesReservedLabels;
2744
+ private edgeReservedLabels;
2734
2745
  /**
2735
2746
  * F6b: mints or renews the org's own files key, or revokes the ones a
2736
2747
  * renewal replaced. The key is minted below the files organisation's owner