@wtfalch/foundry 0.33.1 → 0.35.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 (106) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +1 -1
  2. package/dist/bootstrap/bootstrap.js +49 -11
  3. package/dist/bootstrap/declare.d.ts +12 -5
  4. package/dist/bootstrap/declare.js +33 -18
  5. package/dist/bootstrap/versions.d.ts +24 -0
  6. package/dist/bootstrap/versions.js +46 -0
  7. package/dist/calls-sfu.d.ts +10 -0
  8. package/dist/calls-sfu.js +44 -3
  9. package/dist/index.d.ts +21 -8
  10. package/dist/index.js +11 -4
  11. package/dist/migrations/0051_org_services.sql +13 -0
  12. package/dist/migrations/0052_platform_mail_relay.sql +8 -0
  13. package/dist/migrations/0053_heartbeats_provider.sql +9 -0
  14. package/dist/migrations/0053_org_version_pin.sql +6 -0
  15. package/dist/org.d.ts +76 -12
  16. package/dist/org.js +136 -20
  17. package/dist/plan/backup-gate.d.ts +93 -0
  18. package/dist/plan/backup-gate.js +261 -0
  19. package/dist/plan/broker-entries.d.ts +9 -1
  20. package/dist/plan/broker-entries.js +20 -3
  21. package/dist/plan/build-record.d.ts +56 -0
  22. package/dist/plan/build-record.js +109 -0
  23. package/dist/plan/calls-matrix.d.ts +9 -0
  24. package/dist/plan/calls-matrix.js +18 -1
  25. package/dist/plan/cloudflared-connector.d.ts +7 -1
  26. package/dist/plan/cloudflared-connector.js +15 -2
  27. package/dist/plan/company-backups.d.ts +31 -0
  28. package/dist/plan/company-backups.js +101 -0
  29. package/dist/plan/deploy-health.d.ts +12 -2
  30. package/dist/plan/deploy-health.js +3 -1
  31. package/dist/plan/destinations.d.ts +73 -0
  32. package/dist/plan/destinations.js +217 -0
  33. package/dist/plan/diff.d.ts +46 -8
  34. package/dist/plan/diff.js +367 -126
  35. package/dist/plan/edge-access.d.ts +15 -0
  36. package/dist/plan/edge-access.js +21 -2
  37. package/dist/plan/executor.d.ts +276 -5
  38. package/dist/plan/executor.js +1111 -109
  39. package/dist/plan/health-check-settings.d.ts +28 -0
  40. package/dist/plan/health-check-settings.js +36 -0
  41. package/dist/plan/health-gaps.d.ts +33 -0
  42. package/dist/plan/health-gaps.js +89 -0
  43. package/dist/plan/health-targets.d.ts +1 -3
  44. package/dist/plan/health-targets.js +36 -36
  45. package/dist/plan/held-key-refs.d.ts +15 -0
  46. package/dist/plan/held-key-refs.js +15 -0
  47. package/dist/plan/hostnames.d.ts +10 -0
  48. package/dist/plan/hostnames.js +14 -0
  49. package/dist/plan/index.d.ts +19 -6
  50. package/dist/plan/index.js +10 -3
  51. package/dist/plan/live.d.ts +11 -2
  52. package/dist/plan/live.js +16 -2
  53. package/dist/plan/locked-hostnames.d.ts +7 -3
  54. package/dist/plan/locked-hostnames.js +25 -4
  55. package/dist/plan/login-host.d.ts +61 -12
  56. package/dist/plan/login-host.js +99 -11
  57. package/dist/plan/mail-relay.d.ts +139 -0
  58. package/dist/plan/mail-relay.js +400 -0
  59. package/dist/plan/network-move.d.ts +20 -0
  60. package/dist/plan/network-move.js +41 -0
  61. package/dist/plan/oikos-ingress.d.ts +111 -0
  62. package/dist/plan/oikos-ingress.js +231 -0
  63. package/dist/plan/org-names.d.ts +30 -0
  64. package/dist/plan/org-names.js +108 -0
  65. package/dist/plan/own-diff.d.ts +170 -0
  66. package/dist/plan/own-diff.js +440 -0
  67. package/dist/plan/own-dns.d.ts +6 -0
  68. package/dist/plan/own-dns.js +15 -0
  69. package/dist/plan/own-hostnames.d.ts +27 -0
  70. package/dist/plan/own-hostnames.js +24 -0
  71. package/dist/plan/own-limits.d.ts +213 -0
  72. package/dist/plan/own-limits.js +232 -0
  73. package/dist/plan/own-services.d.ts +193 -0
  74. package/dist/plan/own-services.js +169 -0
  75. package/dist/plan/own-sfu.d.ts +69 -0
  76. package/dist/plan/own-sfu.js +256 -0
  77. package/dist/plan/plan-apply.d.ts +121 -10
  78. package/dist/plan/plan-apply.js +357 -18
  79. package/dist/plan/platform-diff.d.ts +155 -11
  80. package/dist/plan/platform-diff.js +271 -66
  81. package/dist/plan/platform-org.d.ts +107 -4
  82. package/dist/plan/platform-org.js +175 -3
  83. package/dist/plan/platform-providers.d.ts +474 -81
  84. package/dist/plan/platform-providers.js +1407 -560
  85. package/dist/plan/proxy-labels.d.ts +57 -11
  86. package/dist/plan/proxy-labels.js +94 -14
  87. package/dist/plan/run-options.d.ts +19 -0
  88. package/dist/plan/run-options.js +46 -0
  89. package/dist/plan/sign-in-org.d.ts +1 -1
  90. package/dist/plan/sign-in-org.js +20 -2
  91. package/dist/plan/zitadel-login-policy.d.ts +1 -1
  92. package/dist/plan/zitadel-login-policy.js +77 -9
  93. package/dist/plan/zitadel-system-api.d.ts +22 -1
  94. package/dist/plan/zitadel-system-api.js +49 -8
  95. package/dist/platform.d.ts +40 -0
  96. package/dist/platform.js +16 -0
  97. package/dist/provision.d.ts +2 -0
  98. package/dist/provision.js +3 -1
  99. package/dist/slug.d.ts +3 -1
  100. package/dist/slug.js +26 -3
  101. package/dist/store/postgres.js +93 -2
  102. package/dist/store/schema.d.ts +87 -0
  103. package/dist/store/schema.js +9 -0
  104. package/package.json +3 -3
  105. package/versions.digests.json +11 -0
  106. package/versions.json +3 -1
@@ -6,14 +6,22 @@ import { type DeployHealthRuntime } from './deploy-health.js';
6
6
  import type { StepAction } from './diff.js';
7
7
  import type { AccessPort } from './edge-access.js';
8
8
  import type { DnsPort } from './edge-dns.js';
9
+ import type { RateLimitPort } from './edge-ratelimits.js';
9
10
  import type { TunnelPort } from './edge-tunnels.js';
11
+ import type { ZonePort } from './edge-zones.js';
10
12
  import type { CloudflarePort, CoolifyPort, FoundryStepExecutorPorts, ZitadelPort } from './executor.js';
13
+ export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
11
14
  import type { LiveResource, PlatformCloudflareReader, PlatformCoolifyReader } from './live.js';
15
+ import { type OikosIngressPorts } from './oikos-ingress.js';
16
+ import { type FilesKeysMinted, type ProjectSource, type ServiceNetwork, type ServiceTarget, type StepScope } from './own-services.js';
12
17
  import type { PlatformDriftedSteps, PlatformStepExecutionContext, PlatformStepExecutor } from './plan-apply.js';
13
18
  import { type BrokerDeployRecord, PLATFORM_ZITADEL_ORG_SCOPE_KEY } from './platform-diff.js';
19
+ import type { PlatformOwnerInput } from './platform-org.js';
14
20
  export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
15
21
  import { type CallsSfuPlatformDeps } from './platform-sfu.js';
16
22
  import { type PlatformUptimeDeps } from './uptime-worker.js';
23
+ /** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
24
+ export declare function apexZoneKey(apex: string): string;
17
25
  /**
18
26
  * #168: everything {@link provisionPlatformAuth} needs beyond
19
27
  * {@link PlatformStepExecutionContext} -- a bundle, not more constructor
@@ -28,6 +36,15 @@ import { type PlatformUptimeDeps } from './uptime-worker.js';
28
36
  * being the platform key held under {@link PLATFORM_EDGE_KEY_REF}.
29
37
  */
30
38
  export type PlatformDnsFactory = (edgeUrl: string) => DnsPort | Promise<DnsPort>;
39
+ /**
40
+ * The DNS port for a platform step: edge's recorded address, never a public
41
+ * hostname, because auth, mail and archon write their records before any of
42
+ * those exist. Edge's step runs first and records the address
43
+ * (`planPlatform`), so a missing one means that step has not finished.
44
+ */
45
+ export declare function platformDns(deps: {
46
+ dns: PlatformDnsFactory;
47
+ }, recorded: Readonly<Record<string, string>>, step: string): Promise<DnsPort>;
31
48
  /**
32
49
  * F4.tunnels: builds the {@link TunnelPort} a platform step creates, configures
33
50
  * and reads the connector token of its tunnel through, given the same address
@@ -38,6 +55,10 @@ export type PlatformDnsFactory = (edgeUrl: string) => DnsPort | Promise<DnsPort>
38
55
  export type PlatformTunnelsFactory = (edgeUrl: string) => PlatformTunnelPort | Promise<PlatformTunnelPort>;
39
56
  /** The tunnel calls a platform step makes; it never deletes one. */
40
57
  export type PlatformTunnelPort = Pick<TunnelPort, 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken'>;
58
+ /** The tunnel port for a platform step; same address and same reason as {@link platformDns}. */
59
+ export declare function platformTunnels(deps: {
60
+ tunnels: PlatformTunnelsFactory;
61
+ }, recorded: Readonly<Record<string, string>>, step: string): Promise<PlatformTunnelPort>;
41
62
  /**
42
63
  * F4.access: builds the {@link AccessPort} a platform step finds, creates and
43
64
  * deletes its Access app through, given the same address as
@@ -47,7 +68,7 @@ export type PlatformTunnelPort = Pick<TunnelPort, 'findTunnelByName' | 'createTu
47
68
  */
48
69
  export type PlatformAccessFactory = (edgeUrl: string) => PlatformAccessPort | Promise<PlatformAccessPort>;
49
70
  /** The Access calls a platform step makes. */
50
- export type PlatformAccessPort = Pick<AccessPort, 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
71
+ export type PlatformAccessPort = Pick<AccessPort, 'listAccessApps' | 'createAccessApp' | 'ensureOneTimePinProvider' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
51
72
  /**
52
73
  * The SMTP call `provisionPlatformAuth` makes on the instance's ZITADEL.
53
74
  * `@wtfalch/providers` gains it in 0.7.2; stated here so foundry compiles
@@ -91,7 +112,7 @@ export interface PlatformAuthDeps {
91
112
  tunnels: PlatformTunnelsFactory;
92
113
  cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
93
114
  /** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
94
- zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'inviteOrgOwner' | 'addOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp;
115
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'enforceSecondFactor' | 'inviteOrgOwner' | 'addOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp;
95
116
  /**
96
117
  * Mails the owner's first-sign-in link to their personal address (the same
97
118
  * port as `FoundryStepExecutorPorts.ownerInviteMail`). The link carries the
@@ -105,7 +126,7 @@ export interface PlatformAuthDeps {
105
126
  * {@link zitadel}, so the mint always hits the instance this step just
106
127
  * built. See `createZitadelAdminMinter` for the mint half.
107
128
  */
108
- zitadelForInstance?: (issuerUrl: string) => PlatformAuthDeps['zitadel'];
129
+ zitadelForInstance?: (issuerUrl: string, bearerRef?: string) => PlatformAuthDeps['zitadel'];
109
130
  deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
110
131
  /**
111
132
  * #210: reads back the broker's Lettermint mail key, held under
@@ -133,9 +154,19 @@ export interface PlatformAuthRuntime {
133
154
  readyIntervalMs?: number;
134
155
  /** Tries at handing the admin key to the host (default 3). */
135
156
  deliverAttempts?: number;
157
+ /** Timeout of each readiness poll request (default 10 seconds). */
158
+ readyRequestTimeoutMs?: number;
159
+ /** Told of every secret the sign-in step reads or mints, so the host can scrub it from logs and audit rows. */
160
+ onSecret?: (value: string) => void;
136
161
  }
137
- /** The ref the scheduler's admin token is delivered under: the host's real `JobsSchedulerPort` adapter reads it back. */
138
- export declare const JOBS_ADMIN_TOKEN_REF = "jobs:scheduler:adminToken";
162
+ /**
163
+ * An own org's scheduler only: the app's uuid, recorded and persisted before
164
+ * the admin token leaves and cleared once the token is delivered and written.
165
+ * Left set, the planner reports the scheduler blocked and never mints again.
166
+ */
167
+ export declare const JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY = "jobs_admin_token_undelivered";
168
+ /** An own org's scheduler only: the git ref its app last deployed, so a changed declared ref plans the step again. */
169
+ export declare const JOBS_APPLIED_GIT_REF_SCOPE_KEY = "jobs_applied_git_ref";
139
170
  /**
140
171
  * The narrow slice of {@link CoolifyPort} {@link FoundryPlatformStepExecutor}
141
172
  * needs for `coolify:jobs` (#173) -- the same project/environment/server/app/
@@ -143,7 +174,7 @@ export declare const JOBS_ADMIN_TOKEN_REF = "jobs:scheduler:adminToken";
143
174
  * org's own app, reused here for the platform's one scheduler instead of once
144
175
  * per org.
145
176
  */
146
- type JobsCoolifyPort = Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'getApplication' | 'updateApplication' | 'deployAndWait'>;
177
+ export type JobsCoolifyPort = Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'getApplication' | 'updateApplication' | 'deployAndWait'>;
147
178
  /**
148
179
  * Options for {@link FoundryPlatformStepExecutor}'s constructor, replacing
149
180
  * four positional arguments that shifted on every merge that added a
@@ -234,71 +265,12 @@ export interface FoundryPlatformStepExecutorOptions {
234
265
  * gets the step reported as blocked.
235
266
  */
236
267
  accessProvider?: PlatformAccessProviderDeps;
268
+ /**
269
+ * The shared oikos.host ingress step. Optional like `auth`; with edge enabled and this
270
+ * unset, the step is reported blocked.
271
+ */
272
+ oikosIngress?: PlatformOikosIngressDeps;
237
273
  }
238
- /**
239
- * The real {@link PlatformStepExecutor} for the two steps built so far:
240
- * `cloudflare:zone` for the platform's apex (#167) and `coolify:jobs` for the
241
- * jobs scheduler (#173). `coolify` is optional so every #167-era caller --
242
- * anything that only ever plans the apex zone -- keeps constructing this with
243
- * two arguments; a `coolify:jobs` step reaching this executor with no
244
- * `coolify` port configured throws, naming what is missing, rather than
245
- * silently doing nothing.
246
- *
247
- * `cloudflareZone` mirrors `executor.ts`'s `cloudflareZone`: find the zone by
248
- * apex and adopt it, create it only when absent. companyincluded.com already
249
- * exists on Cloudflare, empty, so the first apply adopts it. The owner org
250
- * (Company Included, whose own domain is the apex) may record the same zone
251
- * under its own applied state, so that owner is allowed. Any other org that
252
- * recorded it makes this refuse to adopt. There is no delete here and
253
- * `PlatformStepExecutor` has none: neither path ever removes the zone.
254
- *
255
- * `coolifyJobs` mirrors `executor.ts`'s `coolifyApp`/`coolifyDatabase`,
256
- * simplified for one platform-level singleton rather than one per org: find
257
- * or create a dedicated `platform` Coolify project, find or create the
258
- * scheduler's own database, find or create the scheduler app itself (from
259
- * https://github.com/wtfalch/jobs, one Dockerfile at its repo root, on the
260
- * declared `gitRef`), then push the database's `internal_db_url` in as
261
- * `DATABASE_URL`. There is no park branch yet, matching `planJobs`'s own
262
- * scope note -- `platform.jobs.enabled` going from true to false plans
263
- * nothing, so this method is never asked to stop or delete anything.
264
- *
265
- * #170 adds `sfu:livekit`, the platform's shared calls SFU. `sfu` is
266
- * optional: an existing host that constructs this executor for the zone step
267
- * alone keeps compiling, and a plan that includes an `sfu:livekit` step
268
- * without it configured refuses clearly rather than silently doing nothing.
269
- * #152: the step mints one LiveKit key per org with calls enabled rather
270
- * than one shared pair, so it also needs `declaredOrgStore.list()` -- the
271
- * same store this class already takes for `findOrgIdForZone`.
272
- *
273
- * #168 adds `coolify:app` keyed {@link PLATFORM_AUTH_KEY}, the platform's own
274
- * sign-in ({@link provisionPlatformAuth}), once a host passes
275
- * {@link PlatformAuthDeps}.
276
- *
277
- * #169 adds `coolify:app` keyed {@link PLATFORM_MAIL_KEY}, the platform's own
278
- * mail server ({@link provisionPlatformMail}), once a host passes
279
- * {@link PlatformMailDeps}.
280
- *
281
- * #174 adds `coolify:app` keyed {@link PLATFORM_INTEGRATIONS_KEY},
282
- * the integrations host ({@link provisionPlatformIntegrations}), once a host
283
- * passes {@link PlatformIntegrationsDeps}.
284
- *
285
- * #171 adds `coolify:app` keyed {@link PLATFORM_FILES_KEY}, the files host
286
- * ({@link provisionPlatformFiles}), once a host passes {@link PlatformFilesDeps}.
287
- *
288
- * #172 adds `coolify:app` keyed {@link PLATFORM_AI_KEY}, the platform's AI
289
- * host ({@link provisionPlatformAi}), once a host passes {@link PlatformAiDeps}.
290
- *
291
- * #478 adds `coolify:app` keyed {@link PLATFORM_EDGE_KEY}, the edge host
292
- * ({@link provisionPlatformEdge}), once a host passes {@link PlatformEdgeDeps}.
293
- *
294
- * #175 adds `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, Archon itself
295
- * ({@link provisionPlatformArchon}), once a host passes
296
- * {@link PlatformArchonDeps}.
297
- *
298
- * #257 adds `coolify:backup:<service>`, a nightly R2 backup of each
299
- * platform database ({@link provisionPlatformBackup}), once a host passes
300
- * {@link PlatformBackupDeps}.
301
- */
302
274
  export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor {
303
275
  private readonly cloudflare;
304
276
  private readonly declaredOrgStore;
@@ -318,8 +290,14 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
318
290
  private readonly heartbeats?;
319
291
  private readonly uptime?;
320
292
  private readonly accessProvider?;
293
+ private readonly oikosIngress?;
321
294
  constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list' | 'get'>, options?: FoundryPlatformStepExecutorOptions);
322
- blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
295
+ private readonly platformPorts;
296
+ /** The port placed on the platform's destination, made once per port so a server is listed once. */
297
+ private onPlatform;
298
+ /** `deps` with its Coolify port placed on the platform destination, then limited by `profile`. */
299
+ private limitedOnPlatform;
300
+ blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>, owner?: PlatformOwnerInput): Promise<{
323
301
  auth?: string;
324
302
  authExecutorMissing?: boolean;
325
303
  authOtherInputsMissing?: string[];
@@ -330,7 +308,9 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
330
308
  edge?: string;
331
309
  archon?: string;
332
310
  mail?: string;
311
+ relay?: string;
333
312
  accessProvider?: string;
313
+ oikosIngress?: string;
334
314
  backupExecutorMissing?: boolean;
335
315
  hc?: string;
336
316
  heartbeatsExecutorMissing?: boolean;
@@ -346,6 +326,8 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
346
326
  archonDeployRunning?: string;
347
327
  archonIdTokenUserinfo?: boolean;
348
328
  authSignInPolicy?: boolean;
329
+ archonOrg?: string;
330
+ archonOrgInvite?: string;
349
331
  }>;
350
332
  /**
351
333
  * foundry#542: why the uptime Worker cannot run yet, which optional secrets
@@ -362,7 +344,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
362
344
  * login-client token is no longer the sole live PAT on its ZITADEL machine
363
345
  * user.
364
346
  */
365
- driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<PlatformDriftedSteps>;
347
+ driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>, owner?: PlatformOwnerInput): Promise<PlatformDriftedSteps>;
366
348
  /**
367
349
  * Gives the files runner a changed share-domain set and nothing else: its
368
350
  * Coolify `domains`, `FILES_PUBLIC_HOSTS` and one redeploy of the runner.
@@ -386,7 +368,51 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
386
368
  private executeSfu;
387
369
  private coolifyJobs;
388
370
  }
371
+ export interface JobsHostDeps {
372
+ coolify: JobsCoolifyPort;
373
+ /** `FoundryStepExecutorPorts.deliverSecret`'s contract: never returned, recorded, logged or put in an error. */
374
+ deliverSecret: (ref: string, secret: string) => Promise<void>;
375
+ /** Reads a delivered secret back from the host. Without it, an org's undelivered marker cannot be resumed. */
376
+ readSecret?: (ref: string) => Promise<string | null>;
377
+ runtime: DeployHealthRuntime;
378
+ }
379
+ export interface JobsHostInput {
380
+ /** The scheduler's public domain; null leaves the app on Coolify's own, for a scheduler only containers reach. */
381
+ hostname: string | null;
382
+ gitRef: string;
383
+ project: ProjectSource;
384
+ }
385
+ /**
386
+ * Provisions one jobs scheduler (database, app, admin token) for `target`:
387
+ * the platform's, or an own org's. Every name and the held-key ref come from
388
+ * the target.
389
+ *
390
+ * An org target also guards the mint (`CLAUDE.md`, "Platform steps" rules 1
391
+ * and 3): the token's id is recorded as undelivered and persisted before the
392
+ * token leaves, the delivered guard is recorded after the env write, and the
393
+ * applied git ref is recorded so a changed ref plans the step again. The
394
+ * platform's step keeps its provision-only behaviour.
395
+ */
396
+ export declare function provisionJobsHost(scope: StepScope, deps: JobsHostDeps, target: ServiceTarget, input: JobsHostInput): Promise<{
397
+ resourceId?: string;
398
+ }>;
399
+ /**
400
+ * Fixed `custom_internal_name`s (`diff.ts`'s `coolifyCalls` sets the same
401
+ * kind of name on Synapse/MAS) so the Cloudflare tunnel below can address
402
+ * each app directly, by container name, on the shared Coolify Docker network
403
+ * -- bypassing Coolify's own Traefik, which dispatches purely by `Host`
404
+ * header and so cannot give two different apps the same public hostname.
405
+ */
389
406
  export declare const PLATFORM_AUTH_LOGIN_INTERNAL_NAME = "platform-auth-login";
407
+ /**
408
+ * The inline Dockerfile of the ZITADEL app. The published image is distroless (no shell, no `curl`) and has no
409
+ * HEALTHCHECK, and Coolify runs its own check as `sh -c`. So the app is a Dockerfile app: the image's own `ready`
410
+ * command is its exec-form HEALTHCHECK, which Coolify keeps while its own check is off. `ready` asks the port and
411
+ * TLS mode from the same `ZITADEL_*` settings the server reads. The start period covers the first run, which sets
412
+ * up the database and the first instance before it listens. The start command is the one
413
+ * {@link AUTH_ZITADEL_RUN_OPTIONS} names.
414
+ */
415
+ export declare function zitadelDockerfile(): string;
390
416
  export declare const AUTH_LOGIN_PORT = "3000";
391
417
  /**
392
418
  * Every ZITADEL OIDC/management/console/asset path -- `deploy/proxy/Caddyfile`'s
@@ -396,6 +422,8 @@ export declare const AUTH_LOGIN_PORT = "3000";
396
422
  * everything else, including the root).
397
423
  */
398
424
  export declare const AUTH_ZITADEL_PATH_PATTERN = "^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|ui/console|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)";
425
+ export declare const PLATFORM_AUTH_ZITADEL_SCOPE_KEY = "coolify_platform_auth_zitadel_app";
426
+ export declare const PLATFORM_AUTH_LOGIN_SCOPE_KEY = "coolify_platform_auth_login_app";
399
427
  /**
400
428
  * #283: the ref the new instance's IAM_OWNER machine token (a PAT) is
401
429
  * delivered under, once. A host reads it back to build `zitadelForInstance`
@@ -412,6 +440,101 @@ export declare const PLATFORM_AUTH_INSTANCE_BEARER_REF = "zitadel:platform:insta
412
440
  * (`wtfalch/auth/docs/deploy.md`).
413
441
  */
414
442
  export declare const PLATFORM_AUTH_BROKER_MAIL_KEY_REF = "lettermint:platform:apiToken";
443
+ /** What {@link provisionAuthStack} reads besides its {@link ServiceTarget}: the platform's values, or one company's. */
444
+ export interface AuthStackInput {
445
+ hostname: string;
446
+ apex: string;
447
+ gitRef: string;
448
+ project: ProjectSource;
449
+ network: ServiceNetwork;
450
+ /** The ZITADEL SMTP sender address. */
451
+ smtpSender: string;
452
+ /** Where the login app sends a user with no sign-in request in flight, or null to leave it unset. */
453
+ defaultRedirectUri: string | null;
454
+ /** The company whose owner is invited into its own sign-in org; null for a company's own stack. */
455
+ ownerCompany: DeclaredOrg | null;
456
+ }
457
+ /**
458
+ * #168: makes `coolify:app` keyed {@link PLATFORM_AUTH_KEY} true -- a Coolify
459
+ * Postgres, the ZITADEL app itself, the zitadel-login app and (#204) the
460
+ * sign-in broker, reached through one Cloudflare tunnel and DNS record at
461
+ * `platform.auth.hostname`, then (only the first time) foundry's own admin
462
+ * machine user, handed to {@link PlatformAuthDeps.deliverSecret}. Env values
463
+ * and instance settings are `wtfalch/auth`'s own `docker-compose.yml`
464
+ * (checked 2026-09-29), with `ZITADEL_DOMAIN`/`companyincluded.com` replaced by this
465
+ * platform's own `apex`/`auth.hostname` wherever the compose named one --
466
+ * every other setting it leaves open (login v2 required, external port 443,
467
+ * TLS terminated at Cloudflare, `PASSWORDCHANGEREQUIRED` false) is kept
468
+ * exactly as that compose has it (#168's "Decide"). The estate's old, shared
469
+ * sign-in host carries nothing once this runs (#204, `WTFALCH-DEV-EXIT.md`
470
+ * row A4).
471
+ *
472
+ * Every part is safe to run again after a throw partway: each id is recorded
473
+ * (and persisted) as soon as its resource exists, each app is named and
474
+ * deployed on every run that needs it, and the admin key is delivered before
475
+ * the guard that skips the mint is recorded.
476
+ *
477
+ * Three apps, one public hostname, no separate `proxy` app: `wtfalch/auth`
478
+ * splits ZITADEL's API, its login UI and its broker with a built Caddy proxy
479
+ * container inside one docker-compose stack; foundry instead gives each app a
480
+ * fixed `custom_internal_name` and lets the Cloudflare tunnel itself
481
+ * path-route between them (`AUTH_ZITADEL_PATH_PATTERN`,
482
+ * `AUTH_BROKER_PATH_PATTERN`), skipping Coolify's own Traefik (which can only
483
+ * dispatch by `Host`, not by path) rather than provisioning a fourth Coolify
484
+ * resource to do the same job. No app is created with a Coolify `domains`
485
+ * binding for the same reason -- the tunnel reaches each one directly by its
486
+ * `custom_internal_name`, never through Traefik.
487
+ *
488
+ * Known gaps, left for review (#168's own "the PR stays open for William's
489
+ * review"):
490
+ * - ~~Neither the login app nor the broker ever receives
491
+ * `ZITADEL_SERVICE_USER_TOKEN`.~~ Closed by #216 (building on #209/#215):
492
+ * this function's own login-client block below mints a dedicated
493
+ * `IAM_LOGIN_CLIENT` machine user (never the admin one, which is
494
+ * `IAM_OWNER`) and delivers its token to both apps, once the broker's
495
+ * other required inputs are already met.
496
+ * - ~~The broker never receives the earlier mail provider's token either, so it will not
497
+ * start on `AUTH_APPS` alone.~~ Closed by #210, once `wtfalch/auth#138`
498
+ * moved the broker's account mail onto `@wtfalch/mailer`'s Lettermint
499
+ * transport: this function now reads the key back from
500
+ * {@link PlatformAuthDeps.heldKeys} under
501
+ * {@link PLATFORM_AUTH_BROKER_MAIL_KEY_REF} and pushes it as
502
+ * `LETTERMINT_API_TOKEN`, once a human (or Archon's own onboarding) has
503
+ * put one there. Still a gap until then, the same "blocked, not failing"
504
+ * way an unset `AUTH_APPS` already is.
505
+ * - `AUTH_APPS` starts unset: the broker has nothing to serve until a
506
+ * platform app registers with it ({@link provisionPlatformArchon} is the
507
+ * first), so it will not start (`parseApps` throws on an empty list) until
508
+ * then either. Safe to run again: the next apply that registers an app
509
+ * redeploys the broker with a non-empty list.
510
+ * - ~~Whoever supplies `deps.zitadel` (or `zitadelForInstance`) must hold a
511
+ * bearer token for the new instance, which foundry cannot read from it.~~
512
+ * Closed by #283: see "The instance bearer" below.
513
+ *
514
+ * The instance bearer (#283): the container creates no instance of its own
515
+ * (`ZITADEL_FIRSTINSTANCE_SKIP`). foundry generates an RSA keypair, puts the
516
+ * public half in `ZITADEL_SYSTEMAPIUSERS` (a system API user with
517
+ * SYSTEM_OWNER only), and once the container answers calls ZITADEL's system
518
+ * API `CreateInstance` with a machine owner (IAM_OWNER of the new instance)
519
+ * signed by the private half, which lives in memory for that run only. The
520
+ * owner's PAT comes back once and is handed to
521
+ * {@link PlatformAuthDeps.deliverSecret} under
522
+ * {@link PLATFORM_AUTH_INSTANCE_BEARER_REF}, before anything else uses it. A
523
+ * host builds `zitadelForInstance` as `(url) => createZitadelAdminMinter({
524
+ * url, token: <the secret delivered under that ref> })` plus its own org
525
+ * calls; `zitadelForInstance` is first called after the delivery. No human
526
+ * admin is created: a person who needs console access is added by the host
527
+ * through that bearer.
528
+ *
529
+ * #197: a `cloudflared` connector for this tunnel is run
530
+ * ({@link ensureCloudflaredConnector}), so `hostname` answers.
531
+ */
532
+ export declare function provisionAuthStack(scope: StepScope, deps: PlatformAuthDeps, target: ServiceTarget, input: AuthStackInput): Promise<{
533
+ resourceId?: string;
534
+ }>;
535
+ export declare const PLATFORM_MAIL_INTERNAL_NAME = "platform-mail-stalwart";
536
+ /** Where the server the app was created on is recorded, so the MX record follows the app, not a later pick. */
537
+ export declare const PLATFORM_MAIL_SERVER_SCOPE_KEY = "coolify_platform_mail_server";
415
538
  /**
416
539
  * The fixed description Stalwart's admin API key is minted under
417
540
  * (`StalwartClient.createApiKey`'s own signature returns only the secret,
@@ -443,6 +566,7 @@ export interface PlatformMailAdminPort {
443
566
  * written nothing else. The caller restarts the app and calls again.
444
567
  */
445
568
  applySettings(input: {
569
+ /** The name the server announces on SMTP: its own hostname, never the relay's MX name. */
446
570
  mailHost: string;
447
571
  /** The platform's own apex domain, `platformMailSettingsObjects`'s `usernameDomain`. */
448
572
  apex: string;
@@ -486,7 +610,7 @@ export declare function defaultStalwartForInstance(input: {
486
610
  * own shape.
487
611
  */
488
612
  export interface PlatformMailDeps {
489
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
613
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'restart' | 'getDeployment'>;
490
614
  /** F4.dns: every DNS record call, through edge at its recorded address (see {@link PlatformDnsFactory}). */
491
615
  dns: PlatformDnsFactory;
492
616
  /** F4.tunnels: every tunnel call, through edge at its recorded address (see {@link PlatformTunnelsFactory}). */
@@ -529,6 +653,115 @@ export interface PlatformMailRuntime {
529
653
  /** Tries at handing the admin key to the host (default 3). */
530
654
  deliverAttempts?: number;
531
655
  }
656
+ /**
657
+ * What {@link provisionMailServer} reads besides the target's names: the
658
+ * platform's declared values for the platform's server, the company's for a
659
+ * company's.
660
+ */
661
+ export interface MailServerInput {
662
+ /** Where the server's web, JMAP and management API answer: `mail.<domain>`. */
663
+ hostname: string;
664
+ /** The domain the server's admin account and default domain belong to. */
665
+ apex: string;
666
+ /** The Cloudflare zone `hostname` goes in. Absent: `apex`. A company without a domain has mail domain `<slug>.oikos.host` in zone `oikos.host`. */
667
+ zone?: string;
668
+ /** The MX name: the A record `mxRecord` publishes. The server announces `hostname`, not this. */
669
+ mailHost: string;
670
+ /** The Stalwart image tag. */
671
+ imageTag: string;
672
+ /** `host:container` list; empty publishes no host port. */
673
+ hostPorts: string;
674
+ /** The sign-in the server's directory trusts; its `mail` project's id is the audience. */
675
+ issuerUrl: string;
676
+ /** Publish `mailHost` as an unproxied A record at the server's address. `null`: someone else's record serves it. */
677
+ mxRecord: {
678
+ serverIpv4?: string | null;
679
+ } | null;
680
+ project: ProjectSource;
681
+ network: ServiceNetwork;
682
+ }
683
+ /**
684
+ * #169: makes `coolify:app` keyed {@link PLATFORM_MAIL_KEY} true -- Stalwart
685
+ * on Coolify with its own persistent storage, reached at
686
+ * `platform.mail.hostname` through a Cloudflare tunnel for web, JMAP and its
687
+ * management API (mirroring #168's ZITADEL/login apps: TLS terminated at
688
+ * Cloudflare, plain HTTP behind the tunnel) and at `mx.<apex>` over a direct,
689
+ * unproxied `A` record to the Coolify server's own address for SMTP -- raw
690
+ * SMTP cannot ride Cloudflare's HTTP-only tunnel the way a web app does,
691
+ * which is exactly why #169's own "Human steps" asks a person to open ports
692
+ * 25, 465 and 587 on that server's host firewall. Then, once the instance
693
+ * answers, the OIDC directory against the platform's own sign-in and
694
+ * foundry's admin API key, minted once and handed to
695
+ * {@link PlatformMailDeps.deliverSecret}.
696
+ *
697
+ * Every part is safe to run again after a throw partway, the same rules
698
+ * {@link provisionPlatformAuth} follows: each id is recorded (and persisted)
699
+ * as soon as its resource exists, the app is deployed on every run that
700
+ * needs it, and the admin key is delivered before the guard that skips the
701
+ * mint is recorded. Unlike ZITADEL's `ZITADEL_ADMIN_PASSWORD`, the recovery
702
+ * credential this step generates (`STALWART_RECOVERY_ADMIN`) is read back
703
+ * from Coolify's own env store on every run rather than held only in memory
704
+ * -- `CoolifyPort.listEnvs`'s own doc comment says these values come back
705
+ * live -- so a retry after a delivery failure re-authenticates with the same
706
+ * credential instead of losing it.
707
+ *
708
+ * **Why not `@wtfalch/email/postmaster`'s `mailObjects`/`applyObjects`**, the
709
+ * instance-bootstrap helper `wtfalch-manage` already uses for the hand-built
710
+ * `mail.companyincluded.com`: that plan also wires an `AcmeProvider` and `DnsServer`
711
+ * for Stalwart's own DNS-01 ACME challenge, and an `MtaRoute` that relays
712
+ * every outbound message through the earlier mail provider -- the estate moved transactional
713
+ * mail off it onto Lettermint before this issue (`@wtfalch/mailer` ADR
714
+ * 0006), and this step provisions no Cloudflare DNS token for Stalwart's own
715
+ * ACME either. Calling it as-is would mint an outbound relay this platform
716
+ * does not want and a credential requirement this issue does not cover, so
717
+ * {@link PlatformMailAdminPort.applySettings} is a narrower settings write
718
+ * instead, through `platformMailSettingsObjects`: the default hostname and
719
+ * the OIDC directory only.
720
+ *
721
+ * Known gaps, left for review (like #168's own "the PR stays open for
722
+ * William's review"):
723
+ * - **No ACME/TLS for the SMTP listener.** `mx.<apex>` answers with
724
+ * Stalwart's own self-signed certificate until a follow-up wires a real
725
+ * one (`mailObjects`'s `AcmeProvider`/`DnsServer` shape, minus the earlier relay
726
+ * relay it also carries).
727
+ * - **The Lettermint team token the org app step reads still comes from
728
+ * wherever the host already reads it, not held keys.** Needs
729
+ * `StepExecutionContext` to carry the platform, which `plan-apply.ts`'s
730
+ * own doc comment already scoped out of #167 as more than a small change
731
+ * -- a follow-up issue, not this one. (#205 removed the org step
732
+ * `coolify:mail`'s own fallback to the hand-built mail
733
+ * instance -- `diff.ts`'s `planMail` already blocks the step until the
734
+ * platform's own mail is applied, so the fallback only ever served a
735
+ * plan recorded before that, and now fails loudly instead.)
736
+ * - **A ZITADEL client's token may not carry the mail project's id in
737
+ * `aud` by default.** #346's own "Human steps": confirm this on the first
738
+ * live run, and if it does not, the client must request the mail
739
+ * project's audience scope.
740
+ * - **The admin API key is minted with `'inherit'` permissions**, not a
741
+ * narrower set scoped to only what `applySettings` needs.
742
+ * - **The undelivered-key guard records before `createApiKey` mints**, not
743
+ * after -- so a crash between the two records an id (`admin.createApiKey`
744
+ * has not even run) that no key was ever minted for. Harmless today
745
+ * (the guard only ever blocks a retry and names an id to revoke by), but
746
+ * worth tightening if `createApiKey` ever starts returning its own id.
747
+ */
748
+ export declare function provisionMailServer(scope: StepScope, deps: PlatformMailDeps, target: ServiceTarget, input: MailServerInput): Promise<{
749
+ resourceId?: string;
750
+ }>;
751
+ /**
752
+ * The shared oikos.host ingress step's ports: its Coolify calls, and the tunnel, DNS,
753
+ * rate limit and zone calls, which go through edge at its recorded address like every
754
+ * other platform step's.
755
+ */
756
+ export interface PlatformOikosIngressDeps {
757
+ coolify: OikosIngressPorts['coolify'] & Pick<CoolifyPort, 'listServers'>;
758
+ dns: PlatformDnsFactory;
759
+ tunnels: PlatformTunnelsFactory;
760
+ ratelimits: (edgeUrl: string) => RateLimitPort | Promise<RateLimitPort>;
761
+ zones: (edgeUrl: string) => Pick<ZonePort, 'findZoneByApex'> | Promise<Pick<ZonePort, 'findZoneByApex'>>;
762
+ /** One outside call's limit in milliseconds; 60 s by default. */
763
+ callTimeoutMs?: number;
764
+ }
532
765
  /**
533
766
  * #211: the broker cannot start without `ZITADEL_SERVICE_USER_TOKEN`/`_FILE`,
534
767
  * `LETTERMINT_API_TOKEN` and `AUTH_APPS`. `provisionPlatformAuth` already
@@ -583,8 +816,12 @@ export interface PlatformArchonDeps {
583
816
  cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
584
817
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
585
818
  zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'>;
586
- /** {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client. */
587
- zitadelForInstance?: (issuerUrl: string) => PlatformArchonDeps['zitadel'];
819
+ /**
820
+ * {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client:
821
+ * the owner company's own ZITADEL, by its issuer and the held-key reference of
822
+ * its instance bearer. Required to apply Archon; `zitadel` is never used for it.
823
+ */
824
+ zitadelForInstance?: (issuerUrl: string, bearerRef?: string) => PlatformArchonDeps['zitadel'];
588
825
  /**
589
826
  * Who the Cloudflare Access app in front of Archon lets through. Host
590
827
  * config, not `DeclaredPlatform`: the platform record edge services, not
@@ -652,6 +889,21 @@ export interface PlatformArchonRuntime {
652
889
  * org (`foundry:<orgId>`), with the platform's step key in the org id's place.
653
890
  */
654
891
  export declare const PLATFORM_ARCHON_ACCESS_APP_NAME = "foundry:platform:archon";
892
+ /**
893
+ * Archon paths a machine calls and Archon authenticates itself (its `proxy.ts`:
894
+ * `PUBLIC_EXACT` and `ORG_CHANGES_PATH_RE`). Each gets its own Access app with
895
+ * one bypass policy, because the hostname app would otherwise ask a machine for
896
+ * an emailed code. `/api/uptime/targets` and `/api/uptime/results` are left out
897
+ * on purpose: they pass the hostname app through the uptime service-token
898
+ * policy, and a bypass would make that token meaningless. `*` is one path
899
+ * segment (the organisation id).
900
+ */
901
+ export declare const PLATFORM_ARCHON_PUBLIC_PATHS: ReadonlyArray<{
902
+ name: string;
903
+ path: string;
904
+ }>;
905
+ /** The scope key one bypass app's id is recorded under. */
906
+ export declare const archonPublicAccessScopeKey: (name: string) => string;
655
907
  /** Archon's OIDC client id: public, not a secret, so a plain scope id. */
656
908
  export declare const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = "zitadel_platform_archon_client_id";
657
909
  /**
@@ -688,6 +940,24 @@ type BrokerAppEntry = Record<string, unknown> & {
688
940
  * `TypeError` instead of this named error.
689
941
  */
690
942
  export declare function validateBrokerAppEntry(entry: unknown, brokerAppId: string, index: number): BrokerAppEntry;
943
+ /**
944
+ * Who Archon's Access app lets through. Each person is let in on the personal
945
+ * address their invite was mailed to, never on a work address: the work
946
+ * mailbox is on the platform's own mail, which the platform's own sign-in
947
+ * reaches. So the owner company's first owner's work addresses leave the host's
948
+ * list and their personal address joins it; every other address stays as the
949
+ * host gave it. Case-insensitive, de-duplicated, order kept.
950
+ */
951
+ export declare function archonAccessEmails(hostEmails: readonly string[], ownerCompany: DeclaredOrg | null): string[];
952
+ /**
953
+ * The names of the broker entries that sign in Archon's organisation without
954
+ * being Archon's own. There must be none: a key of another app would then
955
+ * reach Archon's people (passkeys, password reset, deactivation).
956
+ */
957
+ export declare function archonOrgConflicts(entries: ReadonlyArray<{
958
+ name: string;
959
+ organizationId: string;
960
+ }>, archonOrgId: string | undefined): string[];
691
961
  /**
692
962
  * #172/#201: mints Archon's own AI management key -- the plain bearer key
693
963
  * (`wtfk_...`) `AiOrganisationPort` (`executor.ts`) authenticates with,
@@ -758,7 +1028,11 @@ export declare class AiManagementKeyUntrustedError extends Error {
758
1028
  }
759
1029
  /** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
760
1030
  export interface PlatformAiDeps {
761
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
1031
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
1032
+ /** F4.dns: the DNS record of the AI hostname, through edge (see {@link PlatformDnsFactory}). */
1033
+ dns: PlatformDnsFactory;
1034
+ /** F4.tunnels: the AI hostname's tunnel, through edge (see {@link PlatformTunnelsFactory}). */
1035
+ tunnels: PlatformTunnelsFactory;
762
1036
  deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
763
1037
  /**
764
1038
  * Reads back a secret this step delivered on an earlier apply -- needed
@@ -811,8 +1085,6 @@ export interface PlatformAiRuntime {
811
1085
  /** Pause between readiness polls (default 5 seconds). */
812
1086
  readyIntervalMs?: number;
813
1087
  }
814
- /** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
815
- export declare const PLATFORM_AI_MANAGEMENT_KEY_REF = "ai:platform:managementKey";
816
1088
  /**
817
1089
  * What Archon's management key may do in ai: exactly `ai`'s own
818
1090
  * `ARCHON_AI_PERMISSIONS` (`apps/host/scripts/authz/bootstrap-archon.ts`) --
@@ -994,6 +1266,8 @@ export interface PlatformFilesRuntime {
994
1266
  readyTimeoutMs?: number;
995
1267
  /** Pause between readiness polls (default 5 seconds). */
996
1268
  readyIntervalMs?: number;
1269
+ /** Called with every secret this step reads or mints, so the host's run can redact it. */
1270
+ onSecret?: (value: string) => void;
997
1271
  }
998
1272
  /**
999
1273
  * #171: how the host signs a files management request with the key this step
@@ -1007,10 +1281,74 @@ export interface PlatformFilesRuntime {
1007
1281
  */
1008
1282
  export declare const FILES_MANAGEMENT_ISSUER = "foundry";
1009
1283
  export declare const FILES_MANAGEMENT_KEY_ID = "foundry-1";
1010
- /** The ref the management private key (PKCS#8 PEM) is delivered under. */
1011
- export declare const FILES_MANAGEMENT_KEY_REF = "files:platform:managementKey";
1012
1284
  /** The ref the files host's introspection client secret is delivered under. */
1013
1285
  export declare const FILES_INTROSPECTION_SECRET_REF = "zitadel:platform:files:introspection";
1286
+ export declare const PLATFORM_FILES_DB_NAME = "platform-files-db";
1287
+ /** The two Coolify apps files' `apps/host/README.md` ("Deploying on Coolify") names. */
1288
+ export declare const FILES_MIGRATE_APP_NAME = "files-migrate";
1289
+ export declare const FILES_RUNNER_APP_NAME = "files";
1290
+ /**
1291
+ * The runner's fixed `custom_internal_name`: the name another container on
1292
+ * the Coolify network reaches it by, and so the host part of
1293
+ * {@link PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY}'s recorded address.
1294
+ */
1295
+ export declare const FILES_RUNNER_INTERNAL_NAME = "platform-files";
1296
+ export interface FilesHostInput {
1297
+ /** The runner's `APP_URL` host, the management verifier's audience. */
1298
+ hostname: string;
1299
+ gitRef: string;
1300
+ project: ProjectSource;
1301
+ /** `AUTH_ISSUER`, and the sign-in instance the files ZITADEL project is made in. */
1302
+ issuer: string;
1303
+ /** The ZITADEL organisation the files project lives in: found or made by name, or already known by id. */
1304
+ zitadelOrg: {
1305
+ name: string;
1306
+ } | {
1307
+ id: string;
1308
+ };
1309
+ /** The files ZITADEL project (its id is the person-token audience) and the scope key its id is recorded under. */
1310
+ zitadelProject: {
1311
+ name: string;
1312
+ scopeKey: string;
1313
+ };
1314
+ /** {@link filesShareDomains} of the orgs this host serves: each is a runner domain and a public host. */
1315
+ shareDomains: readonly string[];
1316
+ /** An own org's `AUTHZ_BOOTSTRAP_OPERATOR`: the platform owner's id, from the platform's recorded state. */
1317
+ bootstrapOperator?: string;
1318
+ /** An own org's keys credential id: recorded as the guard once both apps carry the keyring. */
1319
+ keysGuardId?: string;
1320
+ }
1321
+ /** The credential id of the keyring an own files host runs on; recorded once both apps carry it. */
1322
+ export declare const OWN_FILES_KEYS_SCOPE_KEY = "files_keys_id";
1323
+ /** The credential id minted and not yet confirmed delivered. */
1324
+ export declare const OWN_FILES_KEYS_UNDELIVERED_SCOPE_KEY = "files_keys_undelivered";
1325
+ /**
1326
+ * An own files host's keyring, before its env writes. Both refs held: those values, no mint (a rerun
1327
+ * after a failed run resumes here). Nothing held: one mint, which is checked, registered as a
1328
+ * secret, recorded by id as undelivered, then delivered to both refs. A re-mint is harmless: it
1329
+ * yields an equivalent credential. Returns the guard id for {@link FilesHostInput.keysGuardId}.
1330
+ */
1331
+ export declare function resolveOwnFilesKeys(scope: StepScope, deps: {
1332
+ deliverSecret: PlatformFilesDeps['deliverSecret'];
1333
+ readSecret?: (ref: string) => Promise<string | null>;
1334
+ runtime?: PlatformFilesDeps['runtime'];
1335
+ }, target: ServiceTarget, mint: () => Promise<FilesKeysMinted>, workerUrl: string, label: string): Promise<{
1336
+ keys: {
1337
+ workerUrl: string;
1338
+ credential: string;
1339
+ verifyKeys: string;
1340
+ };
1341
+ guardId: string;
1342
+ }>;
1343
+ /**
1344
+ * The body of {@link provisionPlatformFiles} for any files host: the
1345
+ * platform's, or an own org's. Every name, the internal name and the two
1346
+ * held-key refs come from `target`; the project, sign-in and ZITADEL home
1347
+ * come from `input`. See {@link provisionPlatformFiles} for what the step does.
1348
+ */
1349
+ export declare function provisionFilesHost(scope: StepScope, deps: PlatformFilesDeps, target: ServiceTarget, input: FilesHostInput): Promise<{
1350
+ resourceId?: string;
1351
+ }>;
1014
1352
  export interface PlatformEdgeRuntime {
1015
1353
  fetch?: typeof fetch;
1016
1354
  sleep?: (ms: number) => Promise<void>;
@@ -1057,13 +1395,21 @@ export interface PlatformAccessProviderDeps {
1057
1395
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
1058
1396
  zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'listApps' | 'deleteApp'> & Required<Pick<ZitadelPort, 'createOidcAppWithSecret'>>;
1059
1397
  /** {@link PlatformArchonDeps.zitadelForInstance}: the ZITADEL client for the sign-in at `issuerUrl`. */
1060
- zitadelForInstance?: (issuerUrl: string) => PlatformAccessProviderDeps['zitadel'];
1398
+ zitadelForInstance?: (issuerUrl: string, bearerRef?: string) => PlatformAccessProviderDeps['zitadel'];
1061
1399
  /** Sleep and delivery tries for the one call that carries the secret; real ones by default. */
1062
1400
  runtime?: {
1063
1401
  sleep?: (ms: number) => Promise<void>;
1064
1402
  deliverAttempts?: number;
1065
1403
  };
1066
1404
  }
1405
+ /**
1406
+ * The only paths the AI host's public route serves; everything else answers 404 at the tunnel. A company entry's key
1407
+ * holds `ai.runs:create` and `ai.offerings:invoke` (`AI_ENTRY_PERMISSIONS`), and `@wtfalch/ai`'s client reaches them
1408
+ * through `POST /v1/runs` (also `?wait=`), `POST /v1/runs/batch`, `GET /v1/runs`, `GET /v1/runs/:id` and
1409
+ * `GET /v1/offerings`. The management routes (`/v1/organisations`, `/v1/keys`, `/v1/budgets`, `/v1/webhooks`,
1410
+ * `/v1/connections`, `/v1/platform/keys`, ...) and `/api/health` are not on the internet.
1411
+ */
1412
+ export declare const AI_ENTRY_PATH_PATTERN = "^/v1/(runs(/|$)|offerings$)";
1067
1413
  export interface PlatformHcRuntime {
1068
1414
  fetch?: typeof fetch;
1069
1415
  sleep?: (ms: number) => Promise<void>;
@@ -1088,6 +1434,20 @@ export interface PlatformHcDeps {
1088
1434
  /** Clock, sleep and fetch for the readiness wait; real ones by default. */
1089
1435
  runtime?: PlatformHcRuntime;
1090
1436
  }
1437
+ export declare function filesKeys(deps: PlatformFilesDeps | undefined): {
1438
+ workerUrl: string;
1439
+ credential: string;
1440
+ verifyKeys: string;
1441
+ } | null;
1442
+ /**
1443
+ * #380: the files host's own checks on what the host supplies, mirrored from
1444
+ * `apps/host/src/lib/authz/files-host.ts` (`R2_ENDPOINT`: a bare https origin)
1445
+ * and `src/lib/keys/config.ts` (`KEYS_VERIFY_KEYS`: 1 or 2 comma-separated
1446
+ * base64url values, each exactly 32 bytes), so a bad value blocks the step
1447
+ * naming the field instead of deploying a host that never turns healthy.
1448
+ * Never echoes the value.
1449
+ */
1450
+ export declare function filesDepsInvalid(deps: PlatformFilesDeps | undefined): string | null;
1091
1451
  /**
1092
1452
  * #174: everything {@link provisionPlatformIntegrations} needs beyond
1093
1453
  * {@link PlatformStepExecutionContext} -- a bundle, like
@@ -1129,6 +1489,8 @@ export declare const INTEGRATIONS_MANAGEMENT_ISSUER = "foundry";
1129
1489
  * its signed request; fixed, since there is one key until rotation is built.
1130
1490
  */
1131
1491
  export declare const INTEGRATIONS_MANAGEMENT_KEY_ID = "foundry-1";
1492
+ /** The uuid `provisionPlatformBackup` records once {@link PLATFORM_BACKUP_S3_STORAGE_NAME} is registered on Coolify, so a later apply never re-registers it. */
1493
+ export declare const PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY = "coolify_platform_backup_s3_storage";
1132
1494
  /**
1133
1495
  * #257: everything {@link provisionPlatformBackup} needs beyond
1134
1496
  * {@link PlatformStepExecutionContext} -- the narrow Coolify slice for the
@@ -1162,6 +1524,27 @@ export interface PlatformBackupDeps {
1162
1524
  /** A cron string or one of Coolify's names (`daily`, `weekly`, ...). Default `'daily'`. */
1163
1525
  frequency?: string;
1164
1526
  }
1527
+ /**
1528
+ * F6: one nightly schedule of one database to a registered S3 storage, the
1529
+ * body `provisionPlatformBackup` had and every company database now shares.
1530
+ * An existing enabled, `save_s3` schedule is reused, never doubled: the lookup
1531
+ * runs before the create, so a run that created a schedule and died before
1532
+ * the caller recorded its uuid finds it again on the next apply. The
1533
+ * `PATCH` names the database to dump, because `createDatabaseBackup` takes no
1534
+ * database name; it also repairs an existing schedule that dumps the wrong
1535
+ * one. Returns the schedule's uuid.
1536
+ */
1537
+ export declare function provisionDatabaseBackup(coolify: Pick<CoolifyPort, 'listDatabaseBackups' | 'createDatabaseBackup' | 'raw'>, input: {
1538
+ databaseUuid: string;
1539
+ /** Names the database in an error, e.g. `platform jobs`. Never a URL or a secret. */
1540
+ label: string;
1541
+ storageUuid: string;
1542
+ frequency?: string;
1543
+ /** The database to dump. Absent: the resource's first database. */
1544
+ databaseName?: string;
1545
+ }): Promise<{
1546
+ uuid: string;
1547
+ }>;
1165
1548
  /** One check as self-hosted Healthchecks' own Management API reports it. */
1166
1549
  export interface HeartbeatCheck {
1167
1550
  id: string;
@@ -1279,6 +1662,16 @@ export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyRe
1279
1662
  constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups' | 'listEnvs' | 'raw'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
1280
1663
  listResources(): Promise<LiveResource[]>;
1281
1664
  }
1665
+ /**
1666
+ * `findZoneByApex` returns the longest suffix match, so for a subdomain it
1667
+ * answers with the parent zone. The platform's zone is its apex exactly;
1668
+ * any other zone is never adopted or reported as the platform's.
1669
+ */
1670
+ export declare function exactZone<Z extends {
1671
+ name: string;
1672
+ }>(client: {
1673
+ findZoneByApex(domain: string): Promise<Z | undefined>;
1674
+ }, apex: string): Promise<Z | undefined>;
1282
1675
  /** Scope id (no `:`): the share domains the runner's Coolify `domains` was last set to, comma-joined. */
1283
1676
  export declare const PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY = "platform_files_share_domains";
1284
1677
  /**