@wtfalch/foundry 0.4.0 → 0.5.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 (66) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +96 -0
  2. package/dist/bootstrap/bootstrap.js +285 -0
  3. package/dist/bootstrap/declare.d.ts +36 -0
  4. package/dist/bootstrap/declare.js +94 -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 +9 -6
  10. package/dist/calls-sfu-deploy-support.js +9 -6
  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 +5 -4
  18. package/dist/calls-sfu.js +5 -4
  19. package/dist/index.d.ts +31 -6
  20. package/dist/index.js +16 -3
  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/org.js +8 -0
  25. package/dist/plan/agora.d.ts +77 -0
  26. package/dist/plan/agora.js +127 -0
  27. package/dist/plan/cloudflared-connector.d.ts +70 -0
  28. package/dist/plan/cloudflared-connector.js +106 -0
  29. package/dist/plan/diff.d.ts +20 -3
  30. package/dist/plan/diff.js +631 -116
  31. package/dist/plan/executor.d.ts +475 -168
  32. package/dist/plan/executor.js +543 -175
  33. package/dist/plan/index.d.ts +22 -5
  34. package/dist/plan/index.js +12 -2
  35. package/dist/plan/jobs-scheduler-http.d.ts +29 -0
  36. package/dist/plan/jobs-scheduler-http.js +50 -0
  37. package/dist/plan/live.d.ts +51 -2
  38. package/dist/plan/live.js +8 -0
  39. package/dist/plan/plan-apply.d.ts +192 -3
  40. package/dist/plan/plan-apply.js +278 -9
  41. package/dist/plan/platform-diff.d.ts +312 -0
  42. package/dist/plan/platform-diff.js +867 -0
  43. package/dist/plan/platform-org-ports.d.ts +50 -0
  44. package/dist/plan/platform-org-ports.js +70 -0
  45. package/dist/plan/platform-org.d.ts +120 -0
  46. package/dist/plan/platform-org.js +115 -0
  47. package/dist/plan/platform-providers.d.ts +501 -0
  48. package/dist/plan/platform-providers.js +3029 -0
  49. package/dist/plan/platform-sfu.d.ts +93 -0
  50. package/dist/plan/platform-sfu.js +100 -0
  51. package/dist/plan/providers-readers.js +22 -1
  52. package/dist/plan/zitadel-machine-user.d.ts +34 -0
  53. package/dist/plan/zitadel-machine-user.js +95 -0
  54. package/dist/plan.d.ts +6 -0
  55. package/dist/platform.d.ts +55 -0
  56. package/dist/platform.js +46 -0
  57. package/dist/requested-changes.d.ts +108 -0
  58. package/dist/requested-changes.js +95 -0
  59. package/dist/slug.js +15 -2
  60. package/dist/store/audit.js +4 -2
  61. package/dist/store/postgres.d.ts +29 -1
  62. package/dist/store/postgres.js +172 -2
  63. package/dist/store/schema.d.ts +683 -0
  64. package/dist/store/schema.js +57 -0
  65. package/dist/store.d.ts +46 -0
  66. package/package.json +1 -1
@@ -0,0 +1,501 @@
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'>;
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
+ *
138
+ * #168 adds `coolify:app` keyed {@link PLATFORM_AUTH_KEY}, the platform's own
139
+ * sign-in ({@link provisionPlatformAuth}), once a host passes
140
+ * {@link PlatformAuthDeps}.
141
+ *
142
+ * #169 adds `coolify:app` keyed {@link PLATFORM_MAIL_KEY}, the platform's own
143
+ * mail server ({@link provisionPlatformMail}), once a host passes
144
+ * {@link PlatformMailDeps}.
145
+ *
146
+ * #174 adds `coolify:app` keyed {@link PLATFORM_INTEGRATIONS_KEY},
147
+ * the integrations host ({@link provisionPlatformIntegrations}), once a host
148
+ * passes {@link PlatformIntegrationsDeps}.
149
+ *
150
+ * #171 adds `coolify:app` keyed {@link PLATFORM_FILES_KEY}, the files host
151
+ * ({@link provisionPlatformFiles}), once a host passes {@link PlatformFilesDeps}.
152
+ *
153
+ * #172 adds `coolify:app` keyed {@link PLATFORM_AI_KEY}, the platform's AI
154
+ * host ({@link provisionPlatformAi}), once a host passes {@link PlatformAiDeps}.
155
+ *
156
+ * #175 adds `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, Archon itself
157
+ * ({@link provisionPlatformArchon}), once a host passes
158
+ * {@link PlatformArchonDeps}.
159
+ */
160
+ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor {
161
+ private readonly cloudflare;
162
+ private readonly declaredOrgStore;
163
+ private readonly coolify?;
164
+ private readonly deliverSecret?;
165
+ private readonly sfu?;
166
+ private readonly auth?;
167
+ private readonly mail?;
168
+ private readonly integrations?;
169
+ private readonly files?;
170
+ private readonly ai?;
171
+ private readonly archon?;
172
+ constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone'>, options?: FoundryPlatformStepExecutorOptions);
173
+ blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
174
+ auth?: string;
175
+ files?: string;
176
+ integrations?: string;
177
+ ai?: string;
178
+ archon?: string;
179
+ mail?: string;
180
+ }>;
181
+ /** Whether Archon's live Access policy lets through anyone but the declared emails. */
182
+ driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
183
+ archon?: boolean;
184
+ }>;
185
+ execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
186
+ resourceId?: string;
187
+ } | undefined>;
188
+ private cloudflareZone;
189
+ private executeSfu;
190
+ private coolifyJobs;
191
+ }
192
+ /**
193
+ * The fixed description Stalwart's admin API key is minted under
194
+ * (`StalwartClient.createApiKey`'s own signature returns only the secret,
195
+ * never an id) -- what a human revokes it by in Stalwart's own admin UI if
196
+ * it is ever lost between mint and delivery, and what
197
+ * {@link PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY} records in that case.
198
+ */
199
+ export declare const PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION = "foundry:platform:admin";
200
+ /**
201
+ * Stalwart's own management API, scoped to exactly what this step needs from
202
+ * the instance it just deployed -- mirrors {@link PlatformAuthDeps}'s
203
+ * `zitadel`/`zitadelForInstance` split: a host builds this from the
204
+ * instance's own URL and the recovery-admin credential this step generated
205
+ * ({@link PlatformMailDeps.stalwartForInstance}), the same way
206
+ * `zitadelForInstance` is built from a freshly issued instance's issuer URL.
207
+ * The real implementation wraps `@wtfalch/email/postmaster`'s `openInstance`
208
+ * (for the `StalwartClient`), a hand-built settings write (see
209
+ * `provisionPlatformMail`'s own doc comment for why not `mailObjects`) and
210
+ * `StalwartClient.createApiKey`.
211
+ */
212
+ export interface PlatformMailAdminPort {
213
+ /**
214
+ * Sets the instance's default hostname and its OIDC directory
215
+ * (`x:SystemSettings`/`x:Directory`/`x:Domain`/`x:Authentication`) --
216
+ * idempotent, safe to call on every apply once the instance is up.
217
+ */
218
+ applySettings(input: {
219
+ mailHost: string;
220
+ oidc: {
221
+ issuerUrl: string;
222
+ audience: string;
223
+ };
224
+ }): Promise<void>;
225
+ /** `StalwartClient.createApiKey`'s own signature. */
226
+ createApiKey(description: string, permissions: readonly string[] | 'inherit'): Promise<string>;
227
+ }
228
+ /**
229
+ * #169: everything {@link provisionPlatformMail} needs beyond
230
+ * {@link PlatformStepExecutionContext}, mirroring {@link PlatformAuthDeps}'s
231
+ * own shape.
232
+ */
233
+ export interface PlatformMailDeps {
234
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
235
+ cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'>;
236
+ /**
237
+ * The ZITADEL resource id an access token's `aud` claim must carry for
238
+ * Stalwart's OIDC directory to accept it -- a resource API registered
239
+ * against the platform's own sign-in (#168), which neither that step nor
240
+ * this one creates. See `provisionPlatformMail`'s own doc comment, "Known
241
+ * gaps".
242
+ */
243
+ oidcAudience: string;
244
+ /**
245
+ * {@link PlatformMailAdminPort}, built from the instance this step just
246
+ * deployed and the recovery credential it just read back.
247
+ */
248
+ stalwartForInstance(input: {
249
+ url: string;
250
+ user: string;
251
+ password: string;
252
+ }): PlatformMailAdminPort;
253
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
254
+ /** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
255
+ runtime?: PlatformMailRuntime;
256
+ }
257
+ export interface PlatformMailRuntime {
258
+ fetch?: typeof fetch;
259
+ sleep?: (ms: number) => Promise<void>;
260
+ now?: () => number;
261
+ /** How long to wait for the new instance to answer (default 5 minutes). */
262
+ readyTimeoutMs?: number;
263
+ /** Pause between readiness polls (default 5 seconds). */
264
+ readyIntervalMs?: number;
265
+ /** Tries at handing the admin key to the host (default 3). */
266
+ deliverAttempts?: number;
267
+ }
268
+ /**
269
+ * #175: everything {@link provisionPlatformArchon} needs beyond
270
+ * {@link PlatformStepExecutionContext}, bundled like {@link PlatformAuthDeps}.
271
+ */
272
+ export interface PlatformArchonDeps {
273
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
274
+ cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord' | 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
275
+ /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
276
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp'>;
277
+ /** {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client. */
278
+ zitadelForInstance?: (issuerUrl: string) => PlatformArchonDeps['zitadel'];
279
+ /**
280
+ * Who the Cloudflare Access app in front of Archon lets through. Host
281
+ * config, not `DeclaredPlatform`: the platform record names services, not
282
+ * people. Empty blocks the step (`blockedSteps`) rather than building an
283
+ * Access app nobody can pass.
284
+ */
285
+ accessEmails: readonly string[];
286
+ /**
287
+ * Reads a secret an earlier step handed to the host through
288
+ * `deliverSecret`, or `null` when none is held under `ref` yet -- the same
289
+ * direction `CallsSfuPort.resolveCredentials` reads held keys back. Every
290
+ * value it returns goes straight to `bulkUpsertEnvs` and nowhere else.
291
+ * Optional so a host without it still compiles; while a service is enabled
292
+ * without it (or without a key it holds) Archon is blocked, not skipped.
293
+ */
294
+ heldKeys?: {
295
+ resolve(ref: string): Promise<string | null>;
296
+ };
297
+ }
298
+ /**
299
+ * The name of foundry's own Cloudflare Access app in front of Archon. Same
300
+ * `foundry:<key>` convention `executor.ts`'s `cloudflareAccess` uses for an
301
+ * org (`foundry:<orgId>`), with the platform's step key in the org id's place.
302
+ */
303
+ export declare const PLATFORM_ARCHON_ACCESS_APP_NAME = "foundry:platform:archon";
304
+ /** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
305
+ export declare const PLATFORM_ZITADEL_ORG_SCOPE_KEY = "zitadel_platform_org";
306
+ /** Archon's OIDC client id: public, not a secret, so a plain scope id. */
307
+ export declare const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = "zitadel_platform_archon_client_id";
308
+ /**
309
+ * #172: mints foundry's AI management key -- the plain bearer key
310
+ * (`wtfk_...`) `AiOrganisationPort` (`executor.ts`) authenticates with,
311
+ * holding {@link PLATFORM_AI_MANAGEMENT_PERMISSIONS} in ai's platform
312
+ * (operator) tenant. The secret comes back once; {@link provisionPlatformAi}
313
+ * hands it to `deliverSecret` and records only the credential id.
314
+ *
315
+ * No implementation ships with this package, because none can be built from
316
+ * it today: see {@link provisionPlatformAi}'s "Known gaps". A host that has
317
+ * a way to run ai's `keys:issue-archon` (or a future HTTP bootstrap route)
318
+ * supplies one; without it the planner reports the step as blocked
319
+ * ({@link PLATFORM_AI_MINTER_BLOCKED}) and plans nothing.
320
+ *
321
+ * Requirement: before issuing anything, a minter must look up an existing
322
+ * management key named `foundry` in the platform tenant and return that one
323
+ * instead of issuing a new one. A crash after `mintManagementKey` returns and
324
+ * before the caller records the credential id leaves a live key nobody knows
325
+ * about; only the minter can find it again, so without this lookup the next
326
+ * apply mints a second key.
327
+ */
328
+ export interface AiManagementKeyMinter {
329
+ mintManagementKey(input: {
330
+ /** Where the ai host answers on the Coolify network, e.g. `http://platform-ai:3000`. */
331
+ internalUrl: string;
332
+ name: string;
333
+ expiresDays: number;
334
+ permissions: readonly string[];
335
+ }): Promise<{
336
+ credentialId: string;
337
+ organisationId: string;
338
+ secret: string;
339
+ }>;
340
+ }
341
+ /** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
342
+ export interface PlatformAiDeps {
343
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
344
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
345
+ /** Optional: see {@link AiManagementKeyMinter}. Absent, the planner blocks the step until the key is delivered. */
346
+ managementKeyMinter?: AiManagementKeyMinter;
347
+ runtime?: PlatformAiRuntime;
348
+ }
349
+ export interface PlatformAiRuntime {
350
+ sleep?: (ms: number) => Promise<void>;
351
+ now?: () => number;
352
+ /** How long to wait for the migrate job to report healthy (default 10 minutes). */
353
+ migrateTimeoutMs?: number;
354
+ /** Pause between migrate-job status polls (default 5 seconds). */
355
+ migrateIntervalMs?: number;
356
+ /** Tries at handing the management key to the host (default 3). */
357
+ deliverAttempts?: number;
358
+ }
359
+ /** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
360
+ export declare const PLATFORM_AI_MANAGEMENT_KEY_REF = "ai:platform:managementKey";
361
+ /**
362
+ * What foundry's management key may do: exactly the grants `ai`'s README
363
+ * ("Archon's first key", by-hand form) gives archon's key, which are the
364
+ * ones `ai:organisation`, `ai:budget`, `ai:key` and the org destroy call.
365
+ */
366
+ export declare const PLATFORM_AI_MANAGEMENT_PERMISSIONS: readonly string[];
367
+ /** The message {@link provisionPlatformAi} throws when no {@link AiManagementKeyMinter} was supplied. */
368
+ export declare const PLATFORM_AI_MINTER_MISSING = "platform:ai: the AI host is deployed but foundry's AI management key cannot be minted: no AiManagementKeyMinter was supplied (PlatformAiDeps.managementKeyMinter). ai mints its first platform credential only through its keys:issue-archon script run against the database (apps/host/scripts/authz/bootstrap-archon.ts); it has no HTTP route for that, and CoolifyPort cannot run a command inside a container (#172)";
369
+ /**
370
+ * #171: everything {@link provisionPlatformFiles} needs beyond
371
+ * {@link PlatformStepExecutionContext}, bundled for the same reason as
372
+ * {@link PlatformAuthDeps}.
373
+ */
374
+ export interface PlatformFilesDeps {
375
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
376
+ /**
377
+ * Must target the platform's own sign-in (the instance `platform:auth`
378
+ * built), or {@link zitadelForInstance} must be set.
379
+ */
380
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createApiApp' | 'listApps' | 'deleteApp'>;
381
+ /**
382
+ * Builds the ZITADEL port for the platform's sign-in from its issuer URL
383
+ * (`https://<auth.hostname>`); used instead of {@link zitadel} when set, the
384
+ * same seam {@link PlatformAuthDeps.zitadelForInstance} is.
385
+ */
386
+ zitadelForInstance?: (issuerUrl: string) => PlatformFilesDeps['zitadel'];
387
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
388
+ /**
389
+ * The keys worker the files host verifies and seals with, from the host's
390
+ * held keys (`KEYS_WORKER_URL`, `KEYS_CREDENTIAL`). The `files-migrate` app
391
+ * never becomes healthy without them, so the step sets both on both apps,
392
+ * and the executor reports the step blocked while this is unset. Never
393
+ * recorded or logged.
394
+ */
395
+ keys?: {
396
+ workerUrl: string;
397
+ credential: string;
398
+ };
399
+ /** Clock and sleep for the migration wait and delivery retries; real ones by default. */
400
+ runtime?: PlatformFilesRuntime;
401
+ }
402
+ export interface PlatformFilesRuntime {
403
+ sleep?: (ms: number) => Promise<void>;
404
+ now?: () => number;
405
+ /** How long to wait for `files-migrate` to report healthy (default 15 minutes: it builds first). */
406
+ migrateTimeoutMs?: number;
407
+ /** Pause between `files-migrate` status polls (default 10 seconds). */
408
+ migrateIntervalMs?: number;
409
+ /** Tries at handing each secret to the host (default 3). */
410
+ deliverAttempts?: number;
411
+ }
412
+ /**
413
+ * #171: how the host signs a files management request with the key this step
414
+ * delivers under {@link FILES_MANAGEMENT_KEY_REF} (files'
415
+ * `signManagementRequest`'s `SigningIdentity`): `issuer` is
416
+ * {@link FILES_MANAGEMENT_ISSUER}, `keyId` is {@link FILES_MANAGEMENT_KEY_ID},
417
+ * `audience` is `https://<platform.files.hostname>` (the files host's
418
+ * `APP_URL`, which this step sets), and `privateKey` is the delivered PKCS#8
419
+ * PEM. Neither constant is a secret. The files host pins the public half
420
+ * through `MANAGEMENT_ISSUER`/`MANAGEMENT_PUBLIC_KEYS`, which this step sets.
421
+ */
422
+ export declare const FILES_MANAGEMENT_ISSUER = "foundry";
423
+ export declare const FILES_MANAGEMENT_KEY_ID = "foundry-1";
424
+ /** The ref the management private key (PKCS#8 PEM) is delivered under. */
425
+ export declare const FILES_MANAGEMENT_KEY_REF = "files:platform:managementKey";
426
+ /** The ref the files host's introspection client secret is delivered under. */
427
+ export declare const FILES_INTROSPECTION_SECRET_REF = "zitadel:platform:files:introspection";
428
+ /**
429
+ * #174: everything {@link provisionPlatformIntegrations} needs beyond
430
+ * {@link PlatformStepExecutionContext} -- a bundle, like
431
+ * {@link PlatformAuthDeps}, so every earlier caller of
432
+ * {@link FoundryPlatformStepExecutor} keeps compiling with it omitted.
433
+ */
434
+ export interface PlatformIntegrationsDeps {
435
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'raw'>;
436
+ /** Hands the management private key to the host, once (`FoundryStepExecutorPorts.deliverSecret`'s contract). */
437
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
438
+ /**
439
+ * The host's own `@wtfalch/keys` settings, written to the apps' env (the
440
+ * migrate app takes the first two). Without every value the planner reports
441
+ * the step blocked: the runner's health check needs them, so the step would
442
+ * never finish. Secrets: only ever passed to `bulkUpsertEnvs`.
443
+ */
444
+ keysEnv: Readonly<Partial<Record<IntegrationsKeysEnvName, string>>>;
445
+ runtime?: PlatformIntegrationsRuntime;
446
+ }
447
+ export declare const INTEGRATIONS_KEYS_ENV_NAMES: readonly ["KEYS_WORKER_URL", "KEYS_CREDENTIAL", "KEYS_VERIFY_KEYS", "KEYS_AUDIT_TOKEN"];
448
+ export type IntegrationsKeysEnvName = (typeof INTEGRATIONS_KEYS_ENV_NAMES)[number];
449
+ export interface PlatformIntegrationsRuntime {
450
+ sleep?: (ms: number) => Promise<void>;
451
+ now?: () => number;
452
+ /** How long to wait for the migrate app to report healthy (default 20 minutes: it builds the whole repo first). */
453
+ migrateTimeoutMs?: number;
454
+ /** Pause between polls of the migrate app's status (default 10 seconds). */
455
+ migratePollMs?: number;
456
+ /** Tries at handing the management key to the host (default 3). */
457
+ deliverAttempts?: number;
458
+ }
459
+ /** The ref the management private key (PKCS#8 PEM) is delivered under. */
460
+ export declare const INTEGRATIONS_MANAGEMENT_KEY_REF = "integrations:management:privateKey";
461
+ /** `MANAGEMENT_ISSUER` on the integrations host: whoever signs with the delivered key signs as this issuer. */
462
+ export declare const INTEGRATIONS_MANAGEMENT_ISSUER = "foundry";
463
+ /**
464
+ * The key id the public half is pinned under in `MANAGEMENT_PUBLIC_KEYS`
465
+ * (the host's keyring allows `^[A-Za-z0-9_-]{1,100}$`). A signer names it in
466
+ * its signed request; fixed, since there is one key until rotation is built.
467
+ */
468
+ export declare const INTEGRATIONS_MANAGEMENT_KEY_ID = "foundry-1";
469
+ /**
470
+ * The real {@link PlatformCloudflareReader}: the platform's apex zone as
471
+ * `planApexZone` expects to see it, and nothing else. The zone's DNS records
472
+ * and tunnels belong to no declared platform step yet, so listing them would
473
+ * only report them unmanaged.
474
+ *
475
+ * The zone is fetched by the id the last apply recorded when there is one,
476
+ * because Cloudflare's zone list can lag a create; with no recorded id it is
477
+ * found by apex.
478
+ */
479
+ export declare class ProvidersPlatformCloudflareReader implements PlatformCloudflareReader {
480
+ private readonly client;
481
+ private readonly platformStore;
482
+ constructor(client: Pick<CloudflareClient, 'findZoneByApex' | 'getZone'>, platformStore: Pick<PlatformStore, 'get' | 'getServiceIds'>);
483
+ listResources(): Promise<LiveResource[]>;
484
+ }
485
+ /**
486
+ * #173: the real {@link PlatformCoolifyReader} for the jobs scheduler app --
487
+ * the recorded-id half of `ProvidersPlatformCloudflareReader`'s own strategy
488
+ * above, with no list-by-name fallback: unlike a Cloudflare zone (which
489
+ * exists before foundry ever touches it), the scheduler app has no identity
490
+ * to adopt until this package creates it, so "nothing recorded yet" and
491
+ * "nothing live yet" are the same state here. A recorded id Coolify no
492
+ * longer has (deleted, or moved) reads as not-live, the same "stale id never
493
+ * blocks planning for good" rule {@link recordedZone} follows.
494
+ */
495
+ export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyReader {
496
+ private readonly client;
497
+ private readonly platformStore;
498
+ constructor(client: Pick<CoolifyPort, 'getApplication'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
499
+ listResources(): Promise<LiveResource[]>;
500
+ }
501
+ export {};