@wtfalch/foundry 0.5.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 (42) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +16 -1
  2. package/dist/bootstrap/bootstrap.js +40 -2
  3. package/dist/bootstrap/declare.js +6 -1
  4. package/dist/calls-sfu-deploy-support.d.ts +8 -0
  5. package/dist/calls-sfu-deploy-support.js +10 -1
  6. package/dist/calls-sfu.d.ts +31 -8
  7. package/dist/calls-sfu.js +36 -5
  8. package/dist/index.d.ts +8 -2
  9. package/dist/index.js +5 -2
  10. package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
  11. package/dist/migrations/0020_mail_apps.sql +7 -0
  12. package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
  13. package/dist/org.d.ts +81 -0
  14. package/dist/org.js +88 -0
  15. package/dist/plan/diff.js +93 -10
  16. package/dist/plan/executor.d.ts +236 -35
  17. package/dist/plan/executor.js +274 -76
  18. package/dist/plan/index.d.ts +7 -1
  19. package/dist/plan/index.js +4 -1
  20. package/dist/plan/mail-copy.d.ts +125 -0
  21. package/dist/plan/mail-copy.js +166 -0
  22. package/dist/plan/mail-identity-allowlist.d.ts +50 -0
  23. package/dist/plan/mail-identity-allowlist.js +11 -0
  24. package/dist/plan/mail-identity.d.ts +68 -0
  25. package/dist/plan/mail-identity.js +17 -0
  26. package/dist/plan/plan-apply.d.ts +5 -0
  27. package/dist/plan/platform-diff.d.ts +184 -9
  28. package/dist/plan/platform-diff.js +368 -15
  29. package/dist/plan/platform-org.d.ts +3 -2
  30. package/dist/plan/platform-providers.d.ts +138 -24
  31. package/dist/plan/platform-providers.js +1460 -79
  32. package/dist/plan/platform-sfu.d.ts +54 -19
  33. package/dist/plan/platform-sfu.js +64 -22
  34. package/dist/plan/zitadel-machine-user.d.ts +60 -11
  35. package/dist/plan/zitadel-machine-user.js +182 -39
  36. package/dist/requested-changes.d.ts +17 -7
  37. package/dist/requested-changes.js +37 -7
  38. package/dist/store/postgres.js +31 -2
  39. package/dist/store/schema.d.ts +72 -0
  40. package/dist/store/schema.js +13 -0
  41. package/dist/store.d.ts +7 -0
  42. package/package.json +1 -1
@@ -18,7 +18,7 @@ export interface PlatformAuthDeps {
18
18
  /** #197: fetches the tunnel's connector token for {@link ensureCloudflaredConnector}. */
19
19
  | 'getTunnelToken'>;
20
20
  /** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
21
- zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser'>;
21
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats'>;
22
22
  /**
23
23
  * Builds the ZITADEL port for the new instance from its issuer URL
24
24
  * (`https://<auth.hostname>`). When set it is used instead of
@@ -134,6 +134,9 @@ export interface FoundryPlatformStepExecutorOptions {
134
134
  * optional: an existing host that constructs this executor for the zone step
135
135
  * alone keeps compiling, and a plan that includes an `sfu:livekit` step
136
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`.
137
140
  *
138
141
  * #168 adds `coolify:app` keyed {@link PLATFORM_AUTH_KEY}, the platform's own
139
142
  * sign-in ({@link provisionPlatformAuth}), once a host passes
@@ -169,23 +172,41 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
169
172
  private readonly files?;
170
173
  private readonly ai?;
171
174
  private readonly archon?;
172
- constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone'>, options?: FoundryPlatformStepExecutorOptions);
175
+ constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list'>, options?: FoundryPlatformStepExecutorOptions);
173
176
  blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
174
177
  auth?: string;
178
+ authExecutorMissing?: boolean;
179
+ authOtherInputsMissing?: string[];
175
180
  files?: string;
176
181
  integrations?: string;
177
182
  ai?: string;
178
183
  archon?: string;
179
184
  mail?: string;
180
185
  }>;
181
- /** Whether Archon's live Access policy lets through anyone but the declared emails. */
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
+ */
182
193
  driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
183
194
  archon?: boolean;
195
+ auth?: boolean;
184
196
  }>;
185
197
  execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
186
198
  resourceId?: string;
187
199
  } | undefined>;
188
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
+ */
189
210
  private executeSfu;
190
211
  private coolifyJobs;
191
212
  }
@@ -290,9 +311,31 @@ export interface PlatformArchonDeps {
290
311
  * value it returns goes straight to `bulkUpsertEnvs` and nowhere else.
291
312
  * Optional so a host without it still compiles; while a service is enabled
292
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.
293
319
  */
294
320
  heldKeys?: {
295
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;
296
339
  };
297
340
  }
298
341
  /**
@@ -306,24 +349,28 @@ export declare const PLATFORM_ZITADEL_ORG_SCOPE_KEY = "zitadel_platform_org";
306
349
  /** Archon's OIDC client id: public, not a secret, so a plain scope id. */
307
350
  export declare const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = "zitadel_platform_archon_client_id";
308
351
  /**
309
- * #172: mints foundry's AI management key -- the plain bearer key
352
+ * #172/#201: mints Archon's own AI management key -- the plain bearer key
310
353
  * (`wtfk_...`) `AiOrganisationPort` (`executor.ts`) authenticates with,
311
354
  * 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.
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.
314
359
  *
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.
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.
320
370
  *
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.
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.
327
374
  */
328
375
  export interface AiManagementKeyMinter {
329
376
  mintManagementKey(input: {
@@ -338,12 +385,55 @@ export interface AiManagementKeyMinter {
338
385
  secret: string;
339
386
  }>;
340
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
+ }
341
419
  /** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
342
420
  export interface PlatformAiDeps {
343
421
  coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
344
422
  deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
345
- /** Optional: see {@link AiManagementKeyMinter}. Absent, the planner blocks the step until the key is delivered. */
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. */
346
434
  managementKeyMinter?: AiManagementKeyMinter;
435
+ /** Injected for tests, e.g. against a fake ai host. Defaults to the global `fetch`. */
436
+ fetchImpl?: typeof fetch;
347
437
  runtime?: PlatformAiRuntime;
348
438
  }
349
439
  export interface PlatformAiRuntime {
@@ -353,19 +443,43 @@ export interface PlatformAiRuntime {
353
443
  migrateTimeoutMs?: number;
354
444
  /** Pause between migrate-job status polls (default 5 seconds). */
355
445
  migrateIntervalMs?: number;
356
- /** Tries at handing the management key to the host (default 3). */
446
+ /** Tries at handing a secret to the host (default 3): the management key and the signing key both use this. */
357
447
  deliverAttempts?: number;
358
448
  }
359
449
  /** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
360
450
  export declare const PLATFORM_AI_MANAGEMENT_KEY_REF = "ai:platform:managementKey";
361
451
  /**
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.
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.
365
458
  */
366
459
  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)";
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;
369
483
  /**
370
484
  * #171: everything {@link provisionPlatformFiles} needs beyond
371
485
  * {@link PlatformStepExecutionContext}, bundled for the same reason as