@wtfalch/foundry 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +111 -0
  2. package/dist/bootstrap/bootstrap.js +323 -0
  3. package/dist/bootstrap/declare.d.ts +36 -0
  4. package/dist/bootstrap/declare.js +99 -0
  5. package/dist/bootstrap/drive.d.ts +40 -0
  6. package/dist/bootstrap/drive.js +51 -0
  7. package/dist/bootstrap/index.d.ts +3 -0
  8. package/dist/bootstrap/index.js +1 -0
  9. package/dist/calls-sfu-deploy-support.d.ts +17 -6
  10. package/dist/calls-sfu-deploy-support.js +19 -7
  11. package/dist/calls-sfu-keys.d.ts +21 -0
  12. package/dist/calls-sfu-keys.js +27 -0
  13. package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
  14. package/dist/calls-sfu-ssh-adapter.js +78 -0
  15. package/dist/calls-sfu-ssh-client.d.ts +49 -0
  16. package/dist/calls-sfu-ssh-client.js +82 -0
  17. package/dist/calls-sfu.d.ts +36 -12
  18. package/dist/calls-sfu.js +41 -9
  19. package/dist/index.d.ts +38 -7
  20. package/dist/index.js +20 -4
  21. package/dist/migrations/0016_platform.sql +61 -0
  22. package/dist/migrations/0017_requested_changes.sql +42 -0
  23. package/dist/migrations/0018_platform_apply.sql +19 -0
  24. package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
  25. package/dist/migrations/0020_mail_apps.sql +7 -0
  26. package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
  27. package/dist/org.d.ts +81 -0
  28. package/dist/org.js +96 -0
  29. package/dist/plan/agora.d.ts +77 -0
  30. package/dist/plan/agora.js +127 -0
  31. package/dist/plan/cloudflared-connector.d.ts +70 -0
  32. package/dist/plan/cloudflared-connector.js +106 -0
  33. package/dist/plan/diff.d.ts +20 -3
  34. package/dist/plan/diff.js +713 -115
  35. package/dist/plan/executor.d.ts +680 -172
  36. package/dist/plan/executor.js +755 -189
  37. package/dist/plan/index.d.ts +28 -5
  38. package/dist/plan/index.js +15 -2
  39. package/dist/plan/jobs-scheduler-http.d.ts +29 -0
  40. package/dist/plan/jobs-scheduler-http.js +50 -0
  41. package/dist/plan/live.d.ts +51 -2
  42. package/dist/plan/live.js +8 -0
  43. package/dist/plan/mail-copy.d.ts +125 -0
  44. package/dist/plan/mail-copy.js +166 -0
  45. package/dist/plan/mail-identity-allowlist.d.ts +50 -0
  46. package/dist/plan/mail-identity-allowlist.js +11 -0
  47. package/dist/plan/mail-identity.d.ts +68 -0
  48. package/dist/plan/mail-identity.js +17 -0
  49. package/dist/plan/plan-apply.d.ts +197 -3
  50. package/dist/plan/plan-apply.js +278 -9
  51. package/dist/plan/platform-diff.d.ts +487 -0
  52. package/dist/plan/platform-diff.js +1220 -0
  53. package/dist/plan/platform-org-ports.d.ts +50 -0
  54. package/dist/plan/platform-org-ports.js +70 -0
  55. package/dist/plan/platform-org.d.ts +121 -0
  56. package/dist/plan/platform-org.js +115 -0
  57. package/dist/plan/platform-providers.d.ts +615 -0
  58. package/dist/plan/platform-providers.js +4410 -0
  59. package/dist/plan/platform-sfu.d.ts +128 -0
  60. package/dist/plan/platform-sfu.js +142 -0
  61. package/dist/plan/providers-readers.js +22 -1
  62. package/dist/plan/zitadel-machine-user.d.ts +83 -0
  63. package/dist/plan/zitadel-machine-user.js +238 -0
  64. package/dist/plan.d.ts +6 -0
  65. package/dist/platform.d.ts +55 -0
  66. package/dist/platform.js +46 -0
  67. package/dist/requested-changes.d.ts +118 -0
  68. package/dist/requested-changes.js +125 -0
  69. package/dist/slug.js +15 -2
  70. package/dist/store/audit.js +4 -2
  71. package/dist/store/postgres.d.ts +29 -1
  72. package/dist/store/postgres.js +203 -4
  73. package/dist/store/schema.d.ts +755 -0
  74. package/dist/store/schema.js +70 -0
  75. package/dist/store.d.ts +53 -0
  76. package/package.json +1 -1
@@ -0,0 +1,615 @@
1
+ import type { CloudflareClient } from '@wtfalch/providers/cloudflare';
2
+ import type { DeclaredPlatform } from '../platform.js';
3
+ import type { DeclaredOrgStore, PlatformStore } from '../store.js';
4
+ import type { StepAction } from './diff.js';
5
+ import type { CloudflarePort, CoolifyPort, FoundryStepExecutorPorts, ZitadelPort } from './executor.js';
6
+ import type { LiveResource, PlatformCloudflareReader, PlatformCoolifyReader } from './live.js';
7
+ import type { PlatformStepExecutionContext, PlatformStepExecutor } from './plan-apply.js';
8
+ import { type CallsSfuPlatformDeps } from './platform-sfu.js';
9
+ /**
10
+ * #168: everything {@link provisionPlatformAuth} needs beyond
11
+ * {@link PlatformStepExecutionContext} -- a bundle, not more constructor
12
+ * positions, so every #167-era caller of {@link FoundryPlatformStepExecutor}
13
+ * (zone only) keeps compiling with this third argument simply omitted.
14
+ */
15
+ export interface PlatformAuthDeps {
16
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
17
+ cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'
18
+ /** #197: fetches the tunnel's connector token for {@link ensureCloudflaredConnector}. */
19
+ | 'getTunnelToken'>;
20
+ /** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
21
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats'>;
22
+ /**
23
+ * Builds the ZITADEL port for the new instance from its issuer URL
24
+ * (`https://<auth.hostname>`). When set it is used instead of
25
+ * {@link zitadel}, so the mint always hits the instance this step just
26
+ * built. See `createZitadelAdminMinter` for the mint half.
27
+ */
28
+ zitadelForInstance?: (issuerUrl: string) => PlatformAuthDeps['zitadel'];
29
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
30
+ /** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
31
+ runtime?: PlatformAuthRuntime;
32
+ }
33
+ export interface PlatformAuthRuntime {
34
+ fetch?: typeof fetch;
35
+ sleep?: (ms: number) => Promise<void>;
36
+ now?: () => number;
37
+ /** How long to wait for the new instance to answer (default 5 minutes). */
38
+ readyTimeoutMs?: number;
39
+ /** Pause between readiness polls (default 5 seconds). */
40
+ readyIntervalMs?: number;
41
+ /** Tries at handing the admin key to the host (default 3). */
42
+ deliverAttempts?: number;
43
+ }
44
+ /** The ref the scheduler's admin token is delivered under: the host's real `JobsSchedulerPort` adapter reads it back. */
45
+ export declare const JOBS_ADMIN_TOKEN_REF = "jobs:scheduler:adminToken";
46
+ /**
47
+ * The narrow slice of {@link CoolifyPort} {@link FoundryPlatformStepExecutor}
48
+ * needs for `coolify:jobs` (#173) -- the same project/environment/server/app/
49
+ * database calls `executor.ts`'s `coolifyApp`/`coolifyDatabase` make for an
50
+ * org's own app, reused here for the platform's one scheduler instead of once
51
+ * per org.
52
+ */
53
+ type JobsCoolifyPort = Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'getApplication' | 'deploy'>;
54
+ /**
55
+ * Options for {@link FoundryPlatformStepExecutor}'s constructor, replacing
56
+ * four positional arguments that shifted on every merge that added a
57
+ * platform service (#186, #187, #188) -- see
58
+ * https://github.com/wtfalch/foundry/pull/188#issuecomment-5895935971, the
59
+ * merge comment that named this shape (#189).
60
+ */
61
+ export interface FoundryPlatformStepExecutorOptions {
62
+ coolify?: JobsCoolifyPort;
63
+ /**
64
+ * Hands a just-generated secret to the host (`FoundryStepExecutorPorts.
65
+ * deliverSecret`'s contract: never returned, recorded, logged or put in
66
+ * an error). Needed by `coolify:jobs` for the scheduler's admin token.
67
+ */
68
+ deliverSecret?: (ref: string, secret: string) => Promise<void>;
69
+ sfu?: CallsSfuPlatformDeps;
70
+ /**
71
+ * #168: dependencies for `coolify:app` keyed {@link PLATFORM_AUTH_KEY} --
72
+ * optional so every #167-era caller (zone only) keeps compiling; a host
73
+ * that declares `platform.auth.enabled` must construct this with it set,
74
+ * or `execute` throws naming what's missing.
75
+ */
76
+ auth?: PlatformAuthDeps;
77
+ /**
78
+ * #169: dependencies for `coolify:app` keyed {@link PLATFORM_MAIL_KEY} --
79
+ * the same optionality as {@link auth}, for the identical reason.
80
+ */
81
+ mail?: PlatformMailDeps;
82
+ /**
83
+ * #174: dependencies for the integrations host, `coolify:app` keyed
84
+ * {@link PLATFORM_INTEGRATIONS_KEY}. Without it, an enabled integrations
85
+ * host is reported blocked by `blockedSteps` rather than planned.
86
+ */
87
+ integrations?: PlatformIntegrationsDeps;
88
+ /**
89
+ * #171: dependencies for `coolify:app` keyed {@link PLATFORM_FILES_KEY} --
90
+ * optional for the same reason as `auth`; a host that declares
91
+ * `platform.files.enabled` without them has the step reported blocked.
92
+ */
93
+ files?: PlatformFilesDeps;
94
+ /**
95
+ * #172: dependencies for `coolify:app` keyed {@link PLATFORM_AI_KEY} --
96
+ * optional for the same reason as `auth`; a host that declares
97
+ * `platform.ai.enabled` without it gets the step reported as blocked.
98
+ */
99
+ ai?: PlatformAiDeps;
100
+ /**
101
+ * #175: dependencies for `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}
102
+ * -- optional for the same reason as {@link auth}.
103
+ */
104
+ archon?: PlatformArchonDeps;
105
+ }
106
+ /**
107
+ * The real {@link PlatformStepExecutor} for the two steps built so far:
108
+ * `cloudflare:zone` for the platform's apex (#167) and `coolify:jobs` for the
109
+ * jobs scheduler (#173). `coolify` is optional so every #167-era caller --
110
+ * anything that only ever plans the apex zone -- keeps constructing this with
111
+ * two arguments; a `coolify:jobs` step reaching this executor with no
112
+ * `coolify` port configured throws, naming what is missing, rather than
113
+ * silently doing nothing.
114
+ *
115
+ * `cloudflareZone` mirrors `executor.ts`'s `cloudflareZone`: find the zone by
116
+ * apex and adopt it, create it only when absent. companyincluded.com already
117
+ * exists on Cloudflare, empty, so the first apply adopts it. The owner org
118
+ * (Company Included, whose own domain is the apex) may record the same zone
119
+ * under its own applied state, so that owner is allowed. Any other org that
120
+ * recorded it makes this refuse to adopt. There is no delete here and
121
+ * `PlatformStepExecutor` has none: neither path ever removes the zone.
122
+ *
123
+ * `coolifyJobs` mirrors `executor.ts`'s `coolifyApp`/`coolifyDatabase`,
124
+ * simplified for one platform-level singleton rather than one per org: find
125
+ * or create a dedicated `platform` Coolify project, find or create the
126
+ * scheduler's own database, find or create the scheduler app itself (from
127
+ * https://github.com/wtfalch/jobs, one Dockerfile at its repo root, on the
128
+ * declared `gitRef`), then push the database's `internal_db_url` in as
129
+ * `DATABASE_URL`. There is no park branch yet, matching `planJobs`'s own
130
+ * scope note -- `platform.jobs.enabled` going from true to false plans
131
+ * nothing, so this method is never asked to stop or delete anything.
132
+ *
133
+ * #170 adds `sfu:livekit`, the platform's shared calls SFU. `sfu` is
134
+ * optional: an existing host that constructs this executor for the zone step
135
+ * alone keeps compiling, and a plan that includes an `sfu:livekit` step
136
+ * without it configured refuses clearly rather than silently doing nothing.
137
+ * #152: the step mints one LiveKit key per org with calls enabled rather
138
+ * than one shared pair, so it also needs `declaredOrgStore.list()` -- the
139
+ * same store this class already takes for `findOrgIdForZone`.
140
+ *
141
+ * #168 adds `coolify:app` keyed {@link PLATFORM_AUTH_KEY}, the platform's own
142
+ * sign-in ({@link provisionPlatformAuth}), once a host passes
143
+ * {@link PlatformAuthDeps}.
144
+ *
145
+ * #169 adds `coolify:app` keyed {@link PLATFORM_MAIL_KEY}, the platform's own
146
+ * mail server ({@link provisionPlatformMail}), once a host passes
147
+ * {@link PlatformMailDeps}.
148
+ *
149
+ * #174 adds `coolify:app` keyed {@link PLATFORM_INTEGRATIONS_KEY},
150
+ * the integrations host ({@link provisionPlatformIntegrations}), once a host
151
+ * passes {@link PlatformIntegrationsDeps}.
152
+ *
153
+ * #171 adds `coolify:app` keyed {@link PLATFORM_FILES_KEY}, the files host
154
+ * ({@link provisionPlatformFiles}), once a host passes {@link PlatformFilesDeps}.
155
+ *
156
+ * #172 adds `coolify:app` keyed {@link PLATFORM_AI_KEY}, the platform's AI
157
+ * host ({@link provisionPlatformAi}), once a host passes {@link PlatformAiDeps}.
158
+ *
159
+ * #175 adds `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, Archon itself
160
+ * ({@link provisionPlatformArchon}), once a host passes
161
+ * {@link PlatformArchonDeps}.
162
+ */
163
+ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor {
164
+ private readonly cloudflare;
165
+ private readonly declaredOrgStore;
166
+ private readonly coolify?;
167
+ private readonly deliverSecret?;
168
+ private readonly sfu?;
169
+ private readonly auth?;
170
+ private readonly mail?;
171
+ private readonly integrations?;
172
+ private readonly files?;
173
+ private readonly ai?;
174
+ private readonly archon?;
175
+ constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list'>, options?: FoundryPlatformStepExecutorOptions);
176
+ blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
177
+ auth?: string;
178
+ authExecutorMissing?: boolean;
179
+ authOtherInputsMissing?: string[];
180
+ files?: string;
181
+ integrations?: string;
182
+ ai?: string;
183
+ archon?: string;
184
+ mail?: string;
185
+ }>;
186
+ /**
187
+ * Whether Archon's live Access policy lets through anyone but the declared
188
+ * emails, or (#211 round 4) the broker it last registered with no longer
189
+ * carries its entry; or (#216 round 4) the platform's recorded
190
+ * login-client token is no longer the sole live PAT on its ZITADEL machine
191
+ * user.
192
+ */
193
+ driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
194
+ archon?: boolean;
195
+ auth?: boolean;
196
+ }>;
197
+ execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
198
+ resourceId?: string;
199
+ } | undefined>;
200
+ private cloudflareZone;
201
+ /**
202
+ * #152: the platform `sfu` step owns the shared LiveKit config, so it
203
+ * lists every org with calls enabled itself -- `DeclaredOrgStore.list()`,
204
+ * filtered the same way an org's own `calls.enabled` flag gates its
205
+ * `coolify:calls-service` step (`executor.ts`'s `coolifyCallsService`) --
206
+ * rather than taking the list as an argument nothing else would supply.
207
+ * Sorted by slug so a re-run with the same set of enabled orgs is a no-op
208
+ * even before `applyCallsSfuPlatformStep`'s own sort (its doc comment).
209
+ */
210
+ private executeSfu;
211
+ private coolifyJobs;
212
+ }
213
+ /**
214
+ * The fixed description Stalwart's admin API key is minted under
215
+ * (`StalwartClient.createApiKey`'s own signature returns only the secret,
216
+ * never an id) -- what a human revokes it by in Stalwart's own admin UI if
217
+ * it is ever lost between mint and delivery, and what
218
+ * {@link PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY} records in that case.
219
+ */
220
+ export declare const PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION = "foundry:platform:admin";
221
+ /**
222
+ * Stalwart's own management API, scoped to exactly what this step needs from
223
+ * the instance it just deployed -- mirrors {@link PlatformAuthDeps}'s
224
+ * `zitadel`/`zitadelForInstance` split: a host builds this from the
225
+ * instance's own URL and the recovery-admin credential this step generated
226
+ * ({@link PlatformMailDeps.stalwartForInstance}), the same way
227
+ * `zitadelForInstance` is built from a freshly issued instance's issuer URL.
228
+ * The real implementation wraps `@wtfalch/email/postmaster`'s `openInstance`
229
+ * (for the `StalwartClient`), a hand-built settings write (see
230
+ * `provisionPlatformMail`'s own doc comment for why not `mailObjects`) and
231
+ * `StalwartClient.createApiKey`.
232
+ */
233
+ export interface PlatformMailAdminPort {
234
+ /**
235
+ * Sets the instance's default hostname and its OIDC directory
236
+ * (`x:SystemSettings`/`x:Directory`/`x:Domain`/`x:Authentication`) --
237
+ * idempotent, safe to call on every apply once the instance is up.
238
+ */
239
+ applySettings(input: {
240
+ mailHost: string;
241
+ oidc: {
242
+ issuerUrl: string;
243
+ audience: string;
244
+ };
245
+ }): Promise<void>;
246
+ /** `StalwartClient.createApiKey`'s own signature. */
247
+ createApiKey(description: string, permissions: readonly string[] | 'inherit'): Promise<string>;
248
+ }
249
+ /**
250
+ * #169: everything {@link provisionPlatformMail} needs beyond
251
+ * {@link PlatformStepExecutionContext}, mirroring {@link PlatformAuthDeps}'s
252
+ * own shape.
253
+ */
254
+ export interface PlatformMailDeps {
255
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
256
+ cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'>;
257
+ /**
258
+ * The ZITADEL resource id an access token's `aud` claim must carry for
259
+ * Stalwart's OIDC directory to accept it -- a resource API registered
260
+ * against the platform's own sign-in (#168), which neither that step nor
261
+ * this one creates. See `provisionPlatformMail`'s own doc comment, "Known
262
+ * gaps".
263
+ */
264
+ oidcAudience: string;
265
+ /**
266
+ * {@link PlatformMailAdminPort}, built from the instance this step just
267
+ * deployed and the recovery credential it just read back.
268
+ */
269
+ stalwartForInstance(input: {
270
+ url: string;
271
+ user: string;
272
+ password: string;
273
+ }): PlatformMailAdminPort;
274
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
275
+ /** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
276
+ runtime?: PlatformMailRuntime;
277
+ }
278
+ export interface PlatformMailRuntime {
279
+ fetch?: typeof fetch;
280
+ sleep?: (ms: number) => Promise<void>;
281
+ now?: () => number;
282
+ /** How long to wait for the new instance to answer (default 5 minutes). */
283
+ readyTimeoutMs?: number;
284
+ /** Pause between readiness polls (default 5 seconds). */
285
+ readyIntervalMs?: number;
286
+ /** Tries at handing the admin key to the host (default 3). */
287
+ deliverAttempts?: number;
288
+ }
289
+ /**
290
+ * #175: everything {@link provisionPlatformArchon} needs beyond
291
+ * {@link PlatformStepExecutionContext}, bundled like {@link PlatformAuthDeps}.
292
+ */
293
+ export interface PlatformArchonDeps {
294
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
295
+ cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord' | 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
296
+ /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
297
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp'>;
298
+ /** {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client. */
299
+ zitadelForInstance?: (issuerUrl: string) => PlatformArchonDeps['zitadel'];
300
+ /**
301
+ * Who the Cloudflare Access app in front of Archon lets through. Host
302
+ * config, not `DeclaredPlatform`: the platform record names services, not
303
+ * people. Empty blocks the step (`blockedSteps`) rather than building an
304
+ * Access app nobody can pass.
305
+ */
306
+ accessEmails: readonly string[];
307
+ /**
308
+ * Reads a secret an earlier step handed to the host through
309
+ * `deliverSecret`, or `null` when none is held under `ref` yet -- the same
310
+ * direction `CallsSfuPort.resolveCredentials` reads held keys back. Every
311
+ * value it returns goes straight to `bulkUpsertEnvs` and nowhere else.
312
+ * Optional so a host without it still compiles; while a service is enabled
313
+ * without it (or without a key it holds) Archon is blocked, not skipped.
314
+ *
315
+ * `put` (#213) writes a value under `ref`, once -- see
316
+ * {@link putArchonMasterKeys}. Also optional, and also blocking: while
317
+ * {@link masterKeys} is given but this cannot put, Archon is blocked, not
318
+ * skipped, the same rule.
319
+ */
320
+ heldKeys?: {
321
+ resolve(ref: string): Promise<string | null>;
322
+ put?(ref: string, value: string): Promise<void>;
323
+ };
324
+ /**
325
+ * #213: the credentials Archon's own onboarding reads back as held keys
326
+ * before it can start (archon#101 round 3, archon#104) -- the Coolify URL
327
+ * and token this package already deploys with, and a Cloudflare parent
328
+ * token with "API Tokens Write" a human creates by hand (#213 "Human
329
+ * steps"), so Archon can mint its own scoped child token from it. Put
330
+ * once, under tenant `platform`, before Archon's app deploys
331
+ * ({@link putArchonMasterKeys}); optional so a host without it still
332
+ * compiles, and Archon is blocked, not skipped, while it (or
333
+ * {@link heldKeys}'s `put`) is missing.
334
+ */
335
+ masterKeys?: {
336
+ coolifyUrl: string;
337
+ coolifyToken: string;
338
+ cloudflareParentToken: string;
339
+ };
340
+ }
341
+ /**
342
+ * The name of foundry's own Cloudflare Access app in front of Archon. Same
343
+ * `foundry:<key>` convention `executor.ts`'s `cloudflareAccess` uses for an
344
+ * org (`foundry:<orgId>`), with the platform's step key in the org id's place.
345
+ */
346
+ export declare const PLATFORM_ARCHON_ACCESS_APP_NAME = "foundry:platform:archon";
347
+ /** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
348
+ export declare const PLATFORM_ZITADEL_ORG_SCOPE_KEY = "zitadel_platform_org";
349
+ /** Archon's OIDC client id: public, not a secret, so a plain scope id. */
350
+ export declare const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = "zitadel_platform_archon_client_id";
351
+ /**
352
+ * #172/#201: mints Archon's own AI management key -- the plain bearer key
353
+ * (`wtfk_...`) `AiOrganisationPort` (`executor.ts`) authenticates with,
354
+ * holding {@link PLATFORM_AI_MANAGEMENT_PERMISSIONS} in ai's platform
355
+ * (operator) tenant, named `archon` (the key is Archon's; foundry only mints
356
+ * it while provisioning -- see {@link provisionPlatformAi}). The secret comes
357
+ * back once; {@link provisionPlatformAi} hands it to `deliverSecret` and
358
+ * records only the credential id.
359
+ *
360
+ * {@link createAiManagementKeyMinter} is the real implementation
361
+ * {@link provisionPlatformAi} builds by default, signing with the Ed25519
362
+ * pair it mints and pins on the runner and calling ai's own
363
+ * `POST /v1/platform/keys` (github.com/wtfalch/ai/issues/141). That route is
364
+ * idempotent by name: a repeat call with the same permissions and expiry
365
+ * returns the live key's id with no secret rather than a second key, so
366
+ * `mintManagementKey` throws {@link AiManagementKeyExistsError} for that
367
+ * case -- {@link provisionPlatformAi} only ever calls it once there is no
368
+ * delivered record, so a 200 here means an earlier mint's secret was lost,
369
+ * never a fresh one to hand back.
370
+ *
371
+ * A test may still inject a different {@link AiManagementKeyMinter} in
372
+ * {@link PlatformAiDeps.managementKeyMinter}, which skips building the
373
+ * default one and its signing-key machinery entirely.
374
+ */
375
+ export interface AiManagementKeyMinter {
376
+ mintManagementKey(input: {
377
+ /** Where the ai host answers on the Coolify network, e.g. `http://platform-ai:3000`. */
378
+ internalUrl: string;
379
+ name: string;
380
+ expiresDays: number;
381
+ permissions: readonly string[];
382
+ }): Promise<{
383
+ credentialId: string;
384
+ organisationId: string;
385
+ secret: string;
386
+ }>;
387
+ }
388
+ /**
389
+ * Thrown by {@link createAiManagementKeyMinter} (never by a test's own
390
+ * injected minter, unless it chooses to) when ai's `POST /v1/platform/keys`
391
+ * answers 200: a live `archon` key already exists and no secret comes back.
392
+ * See {@link PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY}.
393
+ */
394
+ export declare class AiManagementKeyExistsError extends Error {
395
+ readonly credentialId: string;
396
+ readonly organisationId: string;
397
+ constructor(credentialId: string, organisationId: string);
398
+ }
399
+ /**
400
+ * Thrown by {@link createAiManagementKeyMinter} on a 409: a live `archon` key
401
+ * already exists with different permissions or expiry than this request
402
+ * asked for. ai never overwrites it, so this is never retried under another
403
+ * name -- an operator revokes the live key in ai first.
404
+ */
405
+ export declare class AiManagementKeyConflictError extends Error {
406
+ constructor();
407
+ }
408
+ /**
409
+ * Thrown by {@link createAiManagementKeyMinter} on a 401 or 403: ai does not
410
+ * trust foundry's signing key yet for this request. Most likely the runner
411
+ * has not redeployed since {@link AI_SIGNING_KEY_REF}'s public half was
412
+ * pinned as `MANAGEMENT_PUBLIC_KEYS`/`MANAGEMENT_ISSUER` -- a Coolify env
413
+ * change reaches the container only on its next deploy.
414
+ */
415
+ export declare class AiManagementKeyUntrustedError extends Error {
416
+ readonly status: number;
417
+ constructor(status: number);
418
+ }
419
+ /** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
420
+ export interface PlatformAiDeps {
421
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
422
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
423
+ /**
424
+ * Reads back a secret this step delivered on an earlier apply -- needed
425
+ * only to re-sign a mint request when the signing pair was pinned on a
426
+ * run that crashed before the management key itself was minted and
427
+ * delivered (#201). Not needed on the common path, where both happen in
428
+ * the same run.
429
+ */
430
+ heldKeys?: {
431
+ resolve(ref: string): Promise<string | null>;
432
+ };
433
+ /** Injected for tests. Absent, {@link provisionPlatformAi} builds the real {@link createAiManagementKeyMinter} from the signing key it mints. */
434
+ managementKeyMinter?: AiManagementKeyMinter;
435
+ /** Injected for tests, e.g. against a fake ai host. Defaults to the global `fetch`. */
436
+ fetchImpl?: typeof fetch;
437
+ runtime?: PlatformAiRuntime;
438
+ }
439
+ export interface PlatformAiRuntime {
440
+ sleep?: (ms: number) => Promise<void>;
441
+ now?: () => number;
442
+ /** How long to wait for the migrate job to report healthy (default 10 minutes). */
443
+ migrateTimeoutMs?: number;
444
+ /** Pause between migrate-job status polls (default 5 seconds). */
445
+ migrateIntervalMs?: number;
446
+ /** Tries at handing a secret to the host (default 3): the management key and the signing key both use this. */
447
+ deliverAttempts?: number;
448
+ }
449
+ /** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
450
+ export declare const PLATFORM_AI_MANAGEMENT_KEY_REF = "ai:platform:managementKey";
451
+ /**
452
+ * What Archon's management key may do in ai: exactly `ai`'s own
453
+ * `ARCHON_AI_PERMISSIONS` (`apps/host/scripts/authz/bootstrap-archon.ts`) --
454
+ * configuration only (offerings, provisioning, offers, budgets, usage, cost)
455
+ * plus deleting (archiving) an organisation, never connections, runs or
456
+ * webhooks. ai's `/v1/platform/keys` route refuses any permission outside
457
+ * this same list, so a request for more than this is a 403, not a wider key.
458
+ */
459
+ export declare const PLATFORM_AI_MANAGEMENT_PERMISSIONS: readonly string[];
460
+ /** The ref the AI signing key's private half (PKCS#8 PEM) is delivered under -- the {@link INTEGRATIONS_MANAGEMENT_KEY_REF} pattern, for ai instead of integrations. */
461
+ export declare const AI_SIGNING_KEY_REF = "ai:platform:signingPrivateKey";
462
+ /** `MANAGEMENT_ISSUER` on the ai runner: whoever signs with the delivered key signs as this issuer. */
463
+ export declare const AI_MANAGEMENT_ISSUER = "foundry";
464
+ /** The key id the public half is pinned under in `MANAGEMENT_PUBLIC_KEYS` (ai's keyring allows `^[A-Za-z0-9_-]{1,100}$`). Fixed, since there is one key until rotation is built. */
465
+ export declare const AI_MANAGEMENT_KEY_ID = "foundry-1";
466
+ /**
467
+ * The real {@link AiManagementKeyMinter}: signs a management assertion with
468
+ * `privateKeyPem` (the PKCS#8 PEM {@link provisionPlatformAi} delivers under
469
+ * {@link AI_SIGNING_KEY_REF} and pins on the runner under `keyId`) and calls
470
+ * ai's `POST /v1/platform/keys`. `audience` must be the exact origin ai's own
471
+ * `APP_URL` resolves to (`https://<platform.ai.hostname>`), since ai checks
472
+ * the claim against that env var, not against the internal address the
473
+ * request itself is sent to.
474
+ */
475
+ export declare function createAiManagementKeyMinter(options: {
476
+ privateKeyPem: string;
477
+ keyId: string;
478
+ issuer: string;
479
+ audience: string;
480
+ fetchImpl?: typeof fetch;
481
+ now?: () => number;
482
+ }): AiManagementKeyMinter;
483
+ /**
484
+ * #171: everything {@link provisionPlatformFiles} needs beyond
485
+ * {@link PlatformStepExecutionContext}, bundled for the same reason as
486
+ * {@link PlatformAuthDeps}.
487
+ */
488
+ export interface PlatformFilesDeps {
489
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
490
+ /**
491
+ * Must target the platform's own sign-in (the instance `platform:auth`
492
+ * built), or {@link zitadelForInstance} must be set.
493
+ */
494
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createApiApp' | 'listApps' | 'deleteApp'>;
495
+ /**
496
+ * Builds the ZITADEL port for the platform's sign-in from its issuer URL
497
+ * (`https://<auth.hostname>`); used instead of {@link zitadel} when set, the
498
+ * same seam {@link PlatformAuthDeps.zitadelForInstance} is.
499
+ */
500
+ zitadelForInstance?: (issuerUrl: string) => PlatformFilesDeps['zitadel'];
501
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
502
+ /**
503
+ * The keys worker the files host verifies and seals with, from the host's
504
+ * held keys (`KEYS_WORKER_URL`, `KEYS_CREDENTIAL`). The `files-migrate` app
505
+ * never becomes healthy without them, so the step sets both on both apps,
506
+ * and the executor reports the step blocked while this is unset. Never
507
+ * recorded or logged.
508
+ */
509
+ keys?: {
510
+ workerUrl: string;
511
+ credential: string;
512
+ };
513
+ /** Clock and sleep for the migration wait and delivery retries; real ones by default. */
514
+ runtime?: PlatformFilesRuntime;
515
+ }
516
+ export interface PlatformFilesRuntime {
517
+ sleep?: (ms: number) => Promise<void>;
518
+ now?: () => number;
519
+ /** How long to wait for `files-migrate` to report healthy (default 15 minutes: it builds first). */
520
+ migrateTimeoutMs?: number;
521
+ /** Pause between `files-migrate` status polls (default 10 seconds). */
522
+ migrateIntervalMs?: number;
523
+ /** Tries at handing each secret to the host (default 3). */
524
+ deliverAttempts?: number;
525
+ }
526
+ /**
527
+ * #171: how the host signs a files management request with the key this step
528
+ * delivers under {@link FILES_MANAGEMENT_KEY_REF} (files'
529
+ * `signManagementRequest`'s `SigningIdentity`): `issuer` is
530
+ * {@link FILES_MANAGEMENT_ISSUER}, `keyId` is {@link FILES_MANAGEMENT_KEY_ID},
531
+ * `audience` is `https://<platform.files.hostname>` (the files host's
532
+ * `APP_URL`, which this step sets), and `privateKey` is the delivered PKCS#8
533
+ * PEM. Neither constant is a secret. The files host pins the public half
534
+ * through `MANAGEMENT_ISSUER`/`MANAGEMENT_PUBLIC_KEYS`, which this step sets.
535
+ */
536
+ export declare const FILES_MANAGEMENT_ISSUER = "foundry";
537
+ export declare const FILES_MANAGEMENT_KEY_ID = "foundry-1";
538
+ /** The ref the management private key (PKCS#8 PEM) is delivered under. */
539
+ export declare const FILES_MANAGEMENT_KEY_REF = "files:platform:managementKey";
540
+ /** The ref the files host's introspection client secret is delivered under. */
541
+ export declare const FILES_INTROSPECTION_SECRET_REF = "zitadel:platform:files:introspection";
542
+ /**
543
+ * #174: everything {@link provisionPlatformIntegrations} needs beyond
544
+ * {@link PlatformStepExecutionContext} -- a bundle, like
545
+ * {@link PlatformAuthDeps}, so every earlier caller of
546
+ * {@link FoundryPlatformStepExecutor} keeps compiling with it omitted.
547
+ */
548
+ export interface PlatformIntegrationsDeps {
549
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'raw'>;
550
+ /** Hands the management private key to the host, once (`FoundryStepExecutorPorts.deliverSecret`'s contract). */
551
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
552
+ /**
553
+ * The host's own `@wtfalch/keys` settings, written to the apps' env (the
554
+ * migrate app takes the first two). Without every value the planner reports
555
+ * the step blocked: the runner's health check needs them, so the step would
556
+ * never finish. Secrets: only ever passed to `bulkUpsertEnvs`.
557
+ */
558
+ keysEnv: Readonly<Partial<Record<IntegrationsKeysEnvName, string>>>;
559
+ runtime?: PlatformIntegrationsRuntime;
560
+ }
561
+ export declare const INTEGRATIONS_KEYS_ENV_NAMES: readonly ["KEYS_WORKER_URL", "KEYS_CREDENTIAL", "KEYS_VERIFY_KEYS", "KEYS_AUDIT_TOKEN"];
562
+ export type IntegrationsKeysEnvName = (typeof INTEGRATIONS_KEYS_ENV_NAMES)[number];
563
+ export interface PlatformIntegrationsRuntime {
564
+ sleep?: (ms: number) => Promise<void>;
565
+ now?: () => number;
566
+ /** How long to wait for the migrate app to report healthy (default 20 minutes: it builds the whole repo first). */
567
+ migrateTimeoutMs?: number;
568
+ /** Pause between polls of the migrate app's status (default 10 seconds). */
569
+ migratePollMs?: number;
570
+ /** Tries at handing the management key to the host (default 3). */
571
+ deliverAttempts?: number;
572
+ }
573
+ /** The ref the management private key (PKCS#8 PEM) is delivered under. */
574
+ export declare const INTEGRATIONS_MANAGEMENT_KEY_REF = "integrations:management:privateKey";
575
+ /** `MANAGEMENT_ISSUER` on the integrations host: whoever signs with the delivered key signs as this issuer. */
576
+ export declare const INTEGRATIONS_MANAGEMENT_ISSUER = "foundry";
577
+ /**
578
+ * The key id the public half is pinned under in `MANAGEMENT_PUBLIC_KEYS`
579
+ * (the host's keyring allows `^[A-Za-z0-9_-]{1,100}$`). A signer names it in
580
+ * its signed request; fixed, since there is one key until rotation is built.
581
+ */
582
+ export declare const INTEGRATIONS_MANAGEMENT_KEY_ID = "foundry-1";
583
+ /**
584
+ * The real {@link PlatformCloudflareReader}: the platform's apex zone as
585
+ * `planApexZone` expects to see it, and nothing else. The zone's DNS records
586
+ * and tunnels belong to no declared platform step yet, so listing them would
587
+ * only report them unmanaged.
588
+ *
589
+ * The zone is fetched by the id the last apply recorded when there is one,
590
+ * because Cloudflare's zone list can lag a create; with no recorded id it is
591
+ * found by apex.
592
+ */
593
+ export declare class ProvidersPlatformCloudflareReader implements PlatformCloudflareReader {
594
+ private readonly client;
595
+ private readonly platformStore;
596
+ constructor(client: Pick<CloudflareClient, 'findZoneByApex' | 'getZone'>, platformStore: Pick<PlatformStore, 'get' | 'getServiceIds'>);
597
+ listResources(): Promise<LiveResource[]>;
598
+ }
599
+ /**
600
+ * #173: the real {@link PlatformCoolifyReader} for the jobs scheduler app --
601
+ * the recorded-id half of `ProvidersPlatformCloudflareReader`'s own strategy
602
+ * above, with no list-by-name fallback: unlike a Cloudflare zone (which
603
+ * exists before foundry ever touches it), the scheduler app has no identity
604
+ * to adopt until this package creates it, so "nothing recorded yet" and
605
+ * "nothing live yet" are the same state here. A recorded id Coolify no
606
+ * longer has (deleted, or moved) reads as not-live, the same "stale id never
607
+ * blocks planning for good" rule {@link recordedZone} follows.
608
+ */
609
+ export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyReader {
610
+ private readonly client;
611
+ private readonly platformStore;
612
+ constructor(client: Pick<CoolifyPort, 'getApplication'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
613
+ listResources(): Promise<LiveResource[]>;
614
+ }
615
+ export {};