@wtfalch/foundry 0.43.0 → 0.44.1

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.
Files changed (40) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.js +1 -1
  3. package/dist/migrations/0057_org_customers.sql +29 -0
  4. package/dist/org.d.ts +42 -0
  5. package/dist/org.js +52 -26
  6. package/dist/plan/boule.d.ts +43 -2
  7. package/dist/plan/boule.js +48 -1
  8. package/dist/plan/broker-entries.d.ts +28 -0
  9. package/dist/plan/broker-entries.js +46 -1
  10. package/dist/plan/diff.d.ts +14 -0
  11. package/dist/plan/diff.js +176 -18
  12. package/dist/plan/executor.d.ts +100 -2
  13. package/dist/plan/executor.js +273 -16
  14. package/dist/plan/hostnames.d.ts +7 -0
  15. package/dist/plan/hostnames.js +17 -2
  16. package/dist/plan/locked-hostnames.d.ts +11 -0
  17. package/dist/plan/locked-hostnames.js +13 -0
  18. package/dist/plan/mcp.d.ts +57 -0
  19. package/dist/plan/mcp.js +131 -0
  20. package/dist/plan/org-names.d.ts +2 -2
  21. package/dist/plan/org-names.js +16 -1
  22. package/dist/plan/plan-apply.d.ts +26 -0
  23. package/dist/plan/plan-apply.js +45 -5
  24. package/dist/plan/platform-providers.d.ts +1 -1
  25. package/dist/plan/platform-providers.js +2 -1
  26. package/dist/plan/proxy-labels.js +2 -0
  27. package/dist/plan/worker-app-database.d.ts +148 -0
  28. package/dist/plan/worker-app-database.js +424 -0
  29. package/dist/plan/worker-app.d.ts +320 -0
  30. package/dist/plan/worker-app.js +792 -0
  31. package/dist/plan/zitadel-machine-user.d.ts +13 -1
  32. package/dist/plan/zitadel-machine-user.js +80 -0
  33. package/dist/requested-changes.d.ts +12 -3
  34. package/dist/requested-changes.js +22 -0
  35. package/dist/store/postgres.js +12 -6
  36. package/dist/store/schema.d.ts +34 -0
  37. package/dist/store/schema.js +3 -0
  38. package/dist/worker-app-config.d.ts +151 -0
  39. package/dist/worker-app-config.js +438 -0
  40. package/package.json +1 -1
@@ -0,0 +1,320 @@
1
+ import type { DeclaredOrg, HostingEntry } from '../org.js';
2
+ import { type WorkerAppConfig } from '../worker-app-config.js';
3
+ import type { DraftStep, StepAction } from './diff.js';
4
+ import type { DnsPort } from './edge-dns.js';
5
+ import type { ZonePort } from './edge-zones.js';
6
+ import type { StepExecutionContext } from './plan-apply.js';
7
+ import { type WorkerAppDatabaseDeps } from './worker-app-database.js';
8
+ /**
9
+ * ADR 0002: a hosting entry that declares `workerApp` runs as one Cloudflare
10
+ * Worker with Durable Objects and Containers on the platform account. One step
11
+ * per entry, `cloudflare:worker-app`, keyed `<orgId>:<entryKey>`; its resource
12
+ * id is the script name. The module is fetched, checked and uploaded behind
13
+ * {@link WorkerAppPort}; foundry builds and checks out nothing.
14
+ *
15
+ * Order on apply, so a failure at any point leaves a state the next apply
16
+ * repairs and the Worker is never routed before it can serve:
17
+ * trace -> upload (bindings, vars, only the unapplied migrations) -> secrets ->
18
+ * containers -> DNS record (own domain only) -> route -> applied marker.
19
+ * The marker is written last, so a run that fails anywhere is planned again.
20
+ */
21
+ export declare const workerAppAppliedScopeKey: (entryKey: string) => string;
22
+ /** The script name, recorded and persisted before the upload, so a crash mid-deploy leaves a trail to it. */
23
+ export declare const workerAppUploadedScopeKey: (entryKey: string) => string;
24
+ /** The migration tags applied, as a JSON list, recorded after the upload that carried them. */
25
+ export declare const workerAppTagsScopeKey: (entryKey: string) => string;
26
+ /** Secret name -> delivered version, as JSON. Versions only, never a value. */
27
+ export declare const workerAppSecretsScopeKey: (entryKey: string) => string;
28
+ /** The delivered custody credential, as `{hostId, credentialId, expiresAt}` JSON. Never the credential. */
29
+ export declare const workerAppKeysScopeKey: (entryKey: string) => string;
30
+ /** Class name -> `{id, image, instanceType, maxInstances}` of its container application, as JSON. */
31
+ export declare const workerAppContainersScopeKey: (entryKey: string) => string;
32
+ /** The route: `{zoneId, apex, pattern, id}` as JSON. */
33
+ export declare const workerAppRouteScopeKey: (entryKey: string) => string;
34
+ /** A route about to be created, same shape without `id`; recorded and persisted before `createRoute`. */
35
+ export declare const workerAppRoutePendingScopeKey: (entryKey: string) => string;
36
+ /** The proxied record in the org's zone: `{zone, id, name}` as JSON. */
37
+ export declare const workerAppDnsScopeKey: (entryKey: string) => string;
38
+ /** The script name retired, once a retire step is done. */
39
+ export declare const workerAppRetiredScopeKey: (entryKey: string) => string;
40
+ /** Every per-entry scope key that marks something deployed, half deployed or handled. */
41
+ export declare const workerAppTraceScopeKeys: (entryKey: string) => string[];
42
+ /** The recorded id of the step: the script name. */
43
+ export declare const workerAppResourceKey: (stepKey: string) => string;
44
+ export declare const WORKER_APP_ORG_HASH_LENGTH = 8;
45
+ /** `${8 hex of the org id's sha256}-${entry key}-app`. */
46
+ export declare const workerAppName: (org: DeclaredOrg, entry: HostingEntry) => string;
47
+ /** The zone that holds the routes of an org with no domain of its own. */
48
+ export declare const WORKER_APP_OIKOS_ZONE = "oikos.host";
49
+ /** The record a Worker route needs on the org's own zone: Cloudflare's documented dummy, proxied. */
50
+ export declare const WORKER_APP_DNS_RECORD: {
51
+ readonly type: "AAAA";
52
+ readonly content: "100::";
53
+ };
54
+ /** True once the Worker app holds the broker key (`authEnv` delivered it). */
55
+ export declare const brokerKeyDelivered: (recorded: Readonly<Record<string, string>>, entryKey: string) => boolean;
56
+ /**
57
+ * The broker key of a Worker app with `authEnv`, read from the held-key store
58
+ * (it was put there before it was delivered to the Worker). Throws, naming no
59
+ * value, when it is missing or shorter than the broker accepts.
60
+ */
61
+ export declare function heldBrokerKey(heldKeys: {
62
+ resolve(ref: string): Promise<string | null | undefined>;
63
+ } | undefined, org: DeclaredOrg, entryKey: string): Promise<string>;
64
+ /** The foundry-written secret the broker entry shares with the app when `authEnv` is on. */
65
+ export declare const WORKER_APP_AUTH_KEY_SECRET = "AUTH_APP_KEY";
66
+ /**
67
+ * Where foundry keeps a generated secret (the held-key store), so the value
68
+ * is minted once and delivered again unchanged after a crash. The entry key
69
+ * and name are hashed: a held ref is at most 128 characters.
70
+ */
71
+ export declare const workerAppSecretRef: (orgId: string, entryKey: string, name: string) => string;
72
+ export type WorkerAppBinding = {
73
+ type: 'durable_object_namespace';
74
+ name: string;
75
+ class_name: string;
76
+ } | {
77
+ type: 'plain_text';
78
+ name: string;
79
+ text: string;
80
+ } | {
81
+ type: 'hyperdrive';
82
+ name: string;
83
+ id: string;
84
+ };
85
+ export interface WorkerAppMigrations {
86
+ /** The tag the script holds now. Absent for a script with none. */
87
+ oldTag?: string;
88
+ newTag: string;
89
+ steps: {
90
+ newSqliteClasses?: string[];
91
+ deletedClasses?: string[];
92
+ renamedClasses?: {
93
+ from: string;
94
+ to: string;
95
+ }[];
96
+ }[];
97
+ }
98
+ export interface WorkerAppUpload {
99
+ name: string;
100
+ artifactUrl: string;
101
+ artifactSha256: string;
102
+ compatibilityDate: string;
103
+ compatibilityFlags: string[];
104
+ /** All of them: a redeploy replaces the list. Secrets survive a redeploy. */
105
+ bindings: WorkerAppBinding[];
106
+ /** Only the migrations not yet applied; absent when there are none. */
107
+ migrations?: WorkerAppMigrations;
108
+ }
109
+ export interface WorkerAppContainer {
110
+ className: string;
111
+ /** `<repository>@sha256:<digest>`. */
112
+ image: string;
113
+ instanceType: string;
114
+ maxInstances: number;
115
+ }
116
+ /**
117
+ * The Cloudflare calls a Worker app needs. Edge 0.13.0 has none of the Durable
118
+ * Object, migration or container calls, so no adapter ships with this port yet
119
+ * (`createEdgeWorkers` cannot satisfy it); a host without one gets every Worker
120
+ * app reported blocked. Every method is idempotent.
121
+ */
122
+ export interface WorkerAppPort {
123
+ scriptExists(name: string): Promise<boolean>;
124
+ /** The script name bound to `pattern` in `zoneId`, or `undefined`. */
125
+ findRoute(zoneId: string, pattern: string): Promise<{
126
+ id: string;
127
+ script?: string;
128
+ } | undefined>;
129
+ createRoute(zoneId: string, pattern: string, script?: string): Promise<{
130
+ id: string;
131
+ }>;
132
+ /** A no-op when the route is gone. */
133
+ deleteRoute(zoneId: string, routeId: string): Promise<void>;
134
+ /** A no-op when the script is gone. Deleting it removes its secrets. */
135
+ deleteScript(name: string): Promise<void>;
136
+ /** Fetches the module, verifies the sha256 and uploads it with the metadata. */
137
+ uploadWorkerApp(input: WorkerAppUpload): Promise<void>;
138
+ /** The migration tag the live script holds, or `undefined` for none or no script. */
139
+ liveMigrationTag(name: string): Promise<string | undefined>;
140
+ /** A secret whose `version` the script already holds is skipped. */
141
+ setSecrets(name: string, secrets: readonly {
142
+ name: string;
143
+ value: string;
144
+ version: string;
145
+ }[]): Promise<void>;
146
+ /** A no-op when the secret is gone. */
147
+ deleteSecret(name: string, secretName: string): Promise<void>;
148
+ /** Creates or updates the container application of `className` on the script; returns its id. */
149
+ ensureContainerApplication(input: WorkerAppContainer & {
150
+ scriptName: string;
151
+ }): Promise<{
152
+ id: string;
153
+ }>;
154
+ /** A no-op when it is gone. */
155
+ deleteContainerApplication(id: string): Promise<void>;
156
+ }
157
+ /** What the custody Worker returns for a minted derived credential. */
158
+ export interface MintedCustodyCredential {
159
+ credentialId: string;
160
+ /** The secret value. Held and delivered, never printed or recorded. */
161
+ credential: string;
162
+ /** When the credential stops working: an ISO 8601 date-time. */
163
+ expiresAt: string;
164
+ }
165
+ /**
166
+ * The keys custody Worker, for a `keysCustody` secret (ADR 0002). The host
167
+ * wires it to `POST /v1/derived/credential` with the credential of a host in
168
+ * the custody Worker's DERIVE_MINTERS (archon, which runs foundry).
169
+ */
170
+ export interface KeysCustodyPort {
171
+ /** The custody Worker's public URL, delivered as `KEYS_URL`. */
172
+ readonly url: string;
173
+ /** Mints a wrap+unwrap credential (365 days) for the derived host id. */
174
+ mintDerivedCredential(hostId: string): Promise<MintedCustodyCredential>;
175
+ }
176
+ /** Foundry mints a new credential this long before the held one expires. */
177
+ export declare const WORKER_APP_CREDENTIAL_RENEW_BEFORE_MS: number;
178
+ /**
179
+ * True when the delivered custody credential is within 30 days of expiry (or
180
+ * its record is unreadable), so the plan renews it.
181
+ */
182
+ export declare function custodyRenewalDue(recorded: Readonly<Record<string, string>>, entryKey: string, now: Date): boolean;
183
+ export interface WorkerAppWant {
184
+ name: string;
185
+ artifactUrl: string;
186
+ artifactSha256: string;
187
+ compatibilityDate: string;
188
+ compatibilityFlags: string[];
189
+ /** The full hostname the Worker is served at. */
190
+ host: string;
191
+ /** The zone the route and record live in. */
192
+ apex: string;
193
+ classes: {
194
+ binding: string;
195
+ className: string;
196
+ }[];
197
+ migrations: WorkerAppConfig['durableObjects']['migrations'];
198
+ containers: WorkerAppContainer[];
199
+ /** Plain vars, sorted: declared, `authEnv`, and `KEYS_URL`. */
200
+ vars: [string, string][];
201
+ /** `workerApp.database`: the Hyperdrive binding and the tables the runtime role may only append to, sorted. */
202
+ database?: {
203
+ appendOnly: string[];
204
+ migrationsSha256?: string;
205
+ };
206
+ /** Secret names and where each comes from, sorted. */
207
+ secrets: {
208
+ name: string;
209
+ source: 'generated' | 'keysCustody';
210
+ hostId?: string;
211
+ }[];
212
+ }
213
+ /** The zone apex the entry's route lives in. */
214
+ export declare const workerAppApex: (org: DeclaredOrg) => string;
215
+ /**
216
+ * What the Worker must look like, or the inputs still missing (names only).
217
+ * `issuer` is the sign-in issuer the plan read; `keysUrl` the custody Worker.
218
+ */
219
+ export declare function wantedWorkerApp(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>, input: {
220
+ issuer: string | null;
221
+ keysUrl?: string;
222
+ }): {
223
+ want: WorkerAppWant;
224
+ } | {
225
+ missing: string[];
226
+ };
227
+ /**
228
+ * The migration tags the recorded list says are applied must be the first tags
229
+ * of the declared list, in order. `null` when they are; else the refusal.
230
+ */
231
+ export declare function migrationPrefixError(applied: readonly string[], declared: readonly string[]): string | null;
232
+ /** The unapplied migrations after the last tag in `applied`. */
233
+ export declare const unappliedMigrations: (declared: WorkerAppConfig["durableObjects"]["migrations"], applied: readonly string[]) => WorkerAppConfig["durableObjects"]["migrations"];
234
+ type Blocked = Array<{
235
+ description: string;
236
+ key: string;
237
+ }>;
238
+ export interface WorkerAppPlanInput {
239
+ /** The host supplied a {@link WorkerAppPort}. */
240
+ available: boolean;
241
+ /** The custody Worker address, when the host supplied a {@link KeysCustodyPort}. */
242
+ keysUrl?: string;
243
+ /** The clock for credential renewal. Defaults to now. */
244
+ now?: Date;
245
+ /** The host can hold and read secrets (a held-key store that can put). */
246
+ canHoldSecrets: boolean;
247
+ /** The sign-in issuer for `authEnv`. */
248
+ issuer: string | null;
249
+ /** The platform's Cloudflare account id: a container image must live in its registry. */
250
+ registryAccountId?: string;
251
+ /**
252
+ * `workerApp.database`: what the host and the plan have. Absent: the host has no
253
+ * `WorkerAppDatabasePort`, so an entry with `database` is reported blocked.
254
+ */
255
+ database?: {
256
+ /** The org's `coolify:database` step is recorded or planned in this run. */
257
+ databaseReady: boolean;
258
+ /** The org's Cloudflare tunnel is recorded or planned in this run. */
259
+ tunnelReady: boolean;
260
+ /** The executor has Access, tunnel, DNS and zone ports. */
261
+ accessReady: boolean;
262
+ };
263
+ }
264
+ /** The registry prefix a container image of this platform must start with. */
265
+ export declare const workerAppRegistryPrefix: (accountId: string) => string;
266
+ /** Every entry key with a deployed or half-deployed Worker app recorded. */
267
+ export declare function liveWorkerApps(orgId: string, recordedIds: Readonly<Record<string, string>>): Map<string, string>;
268
+ /**
269
+ * One step per Worker app whose declaration or inputs changed, plus a retire
270
+ * step per recorded Worker app no longer declared. Blocked, not planned
271
+ * (`CLAUDE.md` platform rule 2), while the host lacks a port or a value, while
272
+ * the declared migrations contradict the applied ones, and for each input the
273
+ * Worker needs that does not exist yet. A migration that deletes or renames a
274
+ * class makes the step `destroys` or `migrates`, so the org's name must be typed
275
+ * to apply it.
276
+ */
277
+ export declare function planWorkerApps(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, input: WorkerAppPlanInput): {
278
+ steps: DraftStep[];
279
+ blocked: Blocked;
280
+ };
281
+ /** What `applyWorkerApp` needs beyond the context. Each call below is bounded by {@link WORKER_APP_CALL_TIMEOUT_MS}. */
282
+ export interface WorkerAppDeps {
283
+ workers: WorkerAppPort;
284
+ keys?: KeysCustodyPort;
285
+ /** Test hook: the clock for credential renewal. */
286
+ now?: () => Date;
287
+ zones: ZonePort;
288
+ dns: DnsPort;
289
+ heldKeys?: {
290
+ resolve(ref: string): Promise<string | null | undefined>;
291
+ put?(ref: string, value: string): Promise<void>;
292
+ };
293
+ /** `workerApp.database`: the Hyperdrive port and the route's Cloudflare ports. */
294
+ database?: WorkerAppDatabaseDeps;
295
+ /** Test hook: the timeout, in ms. */
296
+ timeoutMs?: number;
297
+ }
298
+ /** An outside call that has not answered in this long fails the step; the next apply repairs it. */
299
+ export declare const WORKER_APP_CALL_TIMEOUT_MS = 120000;
300
+ export declare const WORKER_APP_KEYS_DELIVERY_ATTEMPTS = 3;
301
+ /**
302
+ * Races `task` against a timer. An error names the call, never the cause.
303
+ */
304
+ export declare function withCallTimeout<T>(ms: number, label: string, task: () => Promise<T>): Promise<T>;
305
+ /**
306
+ * Makes the Worker app true. Throws a fixed message on a failure, naming the
307
+ * step and never a value or a provider's cause for anything that carried a
308
+ * secret. Every recorded id is persisted before the call that could lose it.
309
+ */
310
+ export declare function applyWorkerApp(deps: WorkerAppDeps, org: DeclaredOrg, action: StepAction, context: StepExecutionContext): Promise<{
311
+ resourceId?: string;
312
+ }>;
313
+ /**
314
+ * Tears a Worker app down in the order that never leaves a route to nothing:
315
+ * route, record, containers, then the script (its secrets go with it).
316
+ * Held generated secrets stay, so redeclaring reuses them. Used by retire and
317
+ * by org destroy.
318
+ */
319
+ export declare function teardownWorkerApp(deps: WorkerAppDeps, entryKey: string, scriptName: string | undefined, recorded: Readonly<Record<string, string>>, call: <T>(label: string, task: () => Promise<T>) => Promise<T>, orgId: string, context?: Pick<StepExecutionContext, 'recordScopeId' | 'persistScopeIds'> | null): Promise<void>;
320
+ export {};