@wtfalch/foundry 0.7.1 → 0.9.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 (54) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +12 -8
  2. package/dist/bootstrap/bootstrap.js +4 -12
  3. package/dist/bootstrap/declare.js +1 -5
  4. package/dist/bootstrap/versions.d.ts +8 -0
  5. package/dist/bootstrap/versions.js +31 -0
  6. package/dist/calls-sfu-deploy-support.d.ts +1 -1
  7. package/dist/calls-sfu-deploy-support.js +1 -1
  8. package/dist/calls-sfu-ssh-adapter.d.ts +1 -1
  9. package/dist/calls-sfu-ssh-adapter.js +1 -1
  10. package/dist/calls-sfu.d.ts +5 -4
  11. package/dist/calls-sfu.js +5 -4
  12. package/dist/index.d.ts +4 -5
  13. package/dist/index.js +6 -3
  14. package/dist/migrations/0024_audit_events.sql +1 -1
  15. package/dist/migrations/0025_audit_display.sql +1 -1
  16. package/dist/migrations/0026_audit_erase_email_guard.sql +1 -1
  17. package/dist/migrations/0027_audit_chain.sql +1 -1
  18. package/dist/migrations/0028_audit_rls.sql +2 -2
  19. package/dist/migrations/0029_hosting_entries.sql +109 -0
  20. package/dist/migrations-dir.d.ts +9 -0
  21. package/dist/migrations-dir.js +10 -0
  22. package/dist/org.d.ts +59 -27
  23. package/dist/org.js +98 -26
  24. package/dist/plan/calls-matrix.d.ts +2 -0
  25. package/dist/plan/calls-matrix.js +2 -0
  26. package/dist/plan/diff.d.ts +3 -1
  27. package/dist/plan/diff.js +100 -32
  28. package/dist/plan/executor.d.ts +324 -19
  29. package/dist/plan/executor.js +764 -34
  30. package/dist/plan/index.d.ts +1 -1
  31. package/dist/plan/index.js +1 -1
  32. package/dist/plan/login-host.d.ts +24 -0
  33. package/dist/plan/login-host.js +21 -0
  34. package/dist/plan/plan-apply.d.ts +2 -0
  35. package/dist/plan/plan-apply.js +26 -10
  36. package/dist/plan/platform-diff.d.ts +102 -12
  37. package/dist/plan/platform-diff.js +236 -15
  38. package/dist/plan/platform-providers.d.ts +43 -1
  39. package/dist/plan/platform-providers.js +245 -33
  40. package/dist/plan/zitadel-system-api.d.ts +64 -0
  41. package/dist/plan/zitadel-system-api.js +125 -0
  42. package/dist/plan.d.ts +43 -0
  43. package/dist/plan.js +16 -1
  44. package/dist/platform.d.ts +1 -1
  45. package/dist/store/audit.d.ts +13 -17
  46. package/dist/store/audit.js +14 -18
  47. package/dist/store/postgres.d.ts +14 -0
  48. package/dist/store/postgres.js +15 -18
  49. package/dist/store/schema.d.ts +20 -1
  50. package/dist/store/schema.js +7 -1
  51. package/package.json +6 -3
  52. package/versions.json +1 -1
  53. package/dist/migrate.d.ts +0 -38
  54. package/dist/migrate.js +0 -47
@@ -13,7 +13,7 @@ export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, Confirmatio
13
13
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
14
14
  export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './platform-providers.js';
15
15
  export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
16
- export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './platform-providers.js';
16
+ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
17
17
  export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
18
18
  export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './platform-providers.js';
19
19
  export { createZitadelAdminMinter } from './zitadel-machine-user.js';
@@ -6,7 +6,7 @@ export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.
6
6
  export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
7
7
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
8
8
  // #171: how a host signs files management requests with the delivered key.
9
- export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './platform-providers.js';
9
+ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
10
10
  export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
11
11
  export { createZitadelAdminMinter } from './zitadel-machine-user.js';
12
12
  export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
@@ -49,6 +49,14 @@ export interface LoginHostBinding {
49
49
  */
50
50
  export interface LoginHostBindingsPort {
51
51
  upsertBinding(binding: LoginHostBinding): Promise<void>;
52
+ /**
53
+ * #274: removes a host's entry from zitadel-login's own `LOGIN_HOST_BINDINGS`
54
+ * -- the old host's binding, once a host change or an org destroy has
55
+ * moved or removed what it pointed at. Treat "not found" as done, the same
56
+ * rule `executor.ts`'s `isNotFoundError` gives every other removal in this
57
+ * package.
58
+ */
59
+ deleteBinding(host: string): Promise<void>;
52
60
  }
53
61
  /**
54
62
  * Scope id holding what the binding was last written with
@@ -62,3 +70,19 @@ export declare const LOGIN_BINDING_SCOPE_KEY = "login_host_binding";
62
70
  export declare function loginHostClientIds(recorded: Readonly<Record<string, string>>): string[];
63
71
  /** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
64
72
  export declare function loginBindingState(host: string, clientIds: readonly string[]): string;
73
+ /**
74
+ * #274 fix round 2: hosts a company-login-host change (or an org destroy)
75
+ * tried to remove and could not yet -- some live app's own ZITADEL Login V2
76
+ * base URI still pointed at one of them, so removing it would have broken
77
+ * that app's sign-in. `zitadelLoginHost` always advances
78
+ * `zitadel:login-host:<key>` to the org's *current* desired host on every
79
+ * successful run (never leaving it on an old one), so this is the only
80
+ * record of a removal still owed -- lost otherwise the moment a second host
81
+ * change moves `previousHost` on. Comma-joined, sorted, the same shape
82
+ * `MAIL_ROLE_MAILBOXES_SCOPE_KEY` (`executor.ts`) holds a list in.
83
+ */
84
+ export declare const LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY = "zitadel_login_host_pending_removal";
85
+ /** The hosts {@link LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY} currently names, parsed back out. */
86
+ export declare function pendingRemovalHosts(recorded: Readonly<Record<string, string>>): string[];
87
+ /** {@link pendingRemovalHosts}' own serialisation, sorted so the recorded value is stable regardless of check order. */
88
+ export declare function pendingRemovalState(hosts: readonly string[]): string;
@@ -50,3 +50,24 @@ export function loginHostClientIds(recorded) {
50
50
  export function loginBindingState(host, clientIds) {
51
51
  return `${host} ${clientIds.join(',')}`;
52
52
  }
53
+ /**
54
+ * #274 fix round 2: hosts a company-login-host change (or an org destroy)
55
+ * tried to remove and could not yet -- some live app's own ZITADEL Login V2
56
+ * base URI still pointed at one of them, so removing it would have broken
57
+ * that app's sign-in. `zitadelLoginHost` always advances
58
+ * `zitadel:login-host:<key>` to the org's *current* desired host on every
59
+ * successful run (never leaving it on an old one), so this is the only
60
+ * record of a removal still owed -- lost otherwise the moment a second host
61
+ * change moves `previousHost` on. Comma-joined, sorted, the same shape
62
+ * `MAIL_ROLE_MAILBOXES_SCOPE_KEY` (`executor.ts`) holds a list in.
63
+ */
64
+ export const LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY = 'zitadel_login_host_pending_removal';
65
+ /** The hosts {@link LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY} currently names, parsed back out. */
66
+ export function pendingRemovalHosts(recorded) {
67
+ const raw = recorded[LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY];
68
+ return raw ? raw.split(',').filter((host) => host.length > 0) : [];
69
+ }
70
+ /** {@link pendingRemovalHosts}' own serialisation, sorted so the recorded value is stable regardless of check order. */
71
+ export function pendingRemovalState(hosts) {
72
+ return [...hosts].sort().join(',');
73
+ }
@@ -192,6 +192,8 @@ export interface PlatformStepExecutor {
192
192
  ai?: string;
193
193
  archon?: string;
194
194
  mail?: string;
195
+ /** #257: see `PlanPlatformOptions.blocked.backupExecutorMissing` (`platform-diff.ts`). */
196
+ backupExecutorMissing?: boolean;
195
197
  }>;
196
198
  /**
197
199
  * Steps whose live state differs from the declared one in a way the recorded
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
2
3
  import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
3
4
  import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
4
5
  import { readLivePlatformResources, readLiveResources, } from './live.js';
@@ -428,6 +429,15 @@ export class FoundryPlanApply {
428
429
  async sfuBlockedReason(platform) {
429
430
  if (!platform.sfu.enabled)
430
431
  return null;
432
+ // #248: both images the SFU compose actually runs, checked before the
433
+ // connection -- a floating tag is a reason to block independent of
434
+ // whether a connection is held.
435
+ const livekitBlocked = floatingTagBlocked('LiveKit', VERSIONS.livekit, 'v1.13.7');
436
+ if (livekitBlocked)
437
+ return `the platform SFU is blocked, not planned: ${livekitBlocked}`;
438
+ const cloudflaredBlocked = floatingTagBlocked('cloudflared', VERSIONS.cloudflared, '2026.9.3');
439
+ if (cloudflaredBlocked)
440
+ return `the platform SFU is blocked, not planned: ${cloudflaredBlocked}`;
431
441
  const port = this.requirePlatformDeps().sfuConnection;
432
442
  if (!port) {
433
443
  return 'the platform SFU is blocked, not planned: the host supplies no SFU connection port (#170)';
@@ -512,7 +522,10 @@ const ZITADEL_ORG_SCOPE_KEY = 'zitadel_org';
512
522
  /**
513
523
  * Every resource `FoundryPlanApply.destroy` tears down, built from
514
524
  * `AppliedState.serviceIds`, in the order the goal line and this campaign's
515
- * report settle on: the Cloudflare tunnel first, then the Access gate (stop routing traffic to the
525
+ * report settle on: the org's own company login host first (#274 -- an
526
+ * instance-wide ZITADEL trust and a zitadel-login binding, neither depending
527
+ * on anything else this org owns, so nothing is lost by going first), then
528
+ * the Cloudflare tunnel, then the Access gate (stop routing traffic to the
516
529
  * app before tearing it down), then the org's own Coolify database and app,
517
530
  * then the org's customers ZITADEL org (plan gap A9), then every ZITADEL app
518
531
  * foundry recorded, then the ZITADEL project, then the
@@ -541,6 +554,7 @@ function destroyTargets(serviceIds) {
541
554
  // collected in this order, then every ZITADEL app (there can be several,
542
555
  // #71), then the two scope ids last.
543
556
  const ORDERED_PREFIXES = [
557
+ 'zitadel:login-host:',
544
558
  'cloudflare:tunnel:',
545
559
  'cloudflare:access:',
546
560
  'coolify:database:',
@@ -619,13 +633,12 @@ function stepField(action) {
619
633
  *
620
634
  * Three sources, in this order:
621
635
  * - fields no planner step exists for yet, so no apply can make them true:
622
- * SSO, guests and solo when turned on, an adopted app in the `repo` form
623
- * (#7's `#15` comment: "declared but refused by `apply`"), and calls or
624
- * hosting on a domain the org has not recorded (#145: calls on the org's
625
- * own domain now has a planner step, the same way hosting's own domain
626
- * does, so it only fails for that one missing-domain reason, not
627
- * unconditionally). `bespokeAppRepo` is not one of these: `coolify:app`
628
- * reads it (#240), so it converges with that step like any other field;
636
+ * SSO, guests and solo when turned on, and calls or hosting on a domain
637
+ * the org has not recorded (#145: calls on the org's own domain now has a
638
+ * planner step, the same way hosting's own domain does, so it only fails
639
+ * for that one missing-domain reason, not unconditionally). An app repo is
640
+ * not one of these: `coolify:app` reads the `app` entry's `repo`, so it
641
+ * converges with that step like any other field;
629
642
  * - chat's reverse re-key, which the planner surfaces but which is not built
630
643
  * (#7 amendment 3), so it counts as failed even when no step threw;
631
644
  * - on a failed apply, the field of every planned step from the one that
@@ -649,8 +662,6 @@ function failedFields(org, detail, completed, serviceIds) {
649
662
  failed.add('guests.enabled');
650
663
  if (org.solo.enabled)
651
664
  failed.add('solo.enabled');
652
- if (org.adoptedApp?.form === 'repo')
653
- failed.add('adoptedApp');
654
665
  // `planCallsService`/`planTunnel`/`planZone` all share this same domain
655
666
  // requirement (#145): a declared `usesDomain: true` with no recorded
656
667
  // domain plans no step for any of them either.
@@ -689,6 +700,11 @@ function canonical(plan) {
689
700
  orgId: plan.orgId,
690
701
  steps: [...plan.steps].sort((x, y) => x.description.localeCompare(y.description)),
691
702
  unmanaged: [...plan.unmanaged].sort((x, y) => x.description.localeCompare(y.description)),
703
+ // #251: display-only, but still part of what a plan says -- live state
704
+ // moving between `plan()` and `apply()` in a way that only changes a
705
+ // version-change line must still be caught as stale, the same as any
706
+ // other field here.
707
+ versionChanges: [...(plan.versionChanges ?? [])].sort((x, y) => x.description.localeCompare(y.description)),
692
708
  };
693
709
  }
694
710
  /**
@@ -1,5 +1,5 @@
1
- import type { DeclaredOrg, OrgId } from '../org.js';
2
- import type { Plan, UnmanagedItem } from '../plan.js';
1
+ import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
2
+ import { type Plan, type UnmanagedItem, type VersionChangeItem } from '../plan.js';
3
3
  import type { DeclaredPlatform } from '../platform.js';
4
4
  import type { PlannedStep } from './diff.js';
5
5
  import type { LiveResource } from './live.js';
@@ -14,6 +14,8 @@ import type { LiveResource } from './live.js';
14
14
  export interface PlatformPlanDetail {
15
15
  plannedSteps: PlannedStep[];
16
16
  unmanaged: UnmanagedItem[];
17
+ /** #251: see {@link VersionChangeItem}. Never folded into `plannedSteps`. */
18
+ versionChanges: VersionChangeItem[];
17
19
  }
18
20
  /**
19
21
  * The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
@@ -33,6 +35,47 @@ export declare function toPlatformPlan(detail: PlatformPlanDetail, ownerOrgId: O
33
35
  * an org's own (which is keyed by that org's id).
34
36
  */
35
37
  export declare const PLATFORM_AUTH_KEY = "platform:auth";
38
+ /**
39
+ * #251: the ZITADEL image tag the last successful auth run actually set
40
+ * live. `provisionPlatformAuth` records this only when it set the tag --
41
+ * today that is creation alone, `findOrCreatePlatformApp`'s own
42
+ * `justCreated`, since nothing yet patches `docker_registry_image_tag` on
43
+ * an app found already running (unlike mail, which does). Recording it on
44
+ * every run regardless, the way the app id itself is recorded, would make
45
+ * {@link versionNotAppliedLine} compare the pinned version against a value
46
+ * that was never actually applied.
47
+ */
48
+ export declare const PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY = "auth_zitadel_applied_image_tag";
49
+ /**
50
+ * #251: the Synapse/MAS image tags an org's calls Matrix stack last
51
+ * successful apply actually set live. `coolifyCalls` (`executor.ts`)
52
+ * records these only at creation, for the same reason
53
+ * {@link PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY} is: nothing yet
54
+ * patches either tag on an app `findOrCreateCallsApp` found already
55
+ * running.
56
+ */
57
+ export declare const CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY = "calls_synapse_applied_image_tag";
58
+ export declare const CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY = "calls_mas_applied_image_tag";
59
+ /**
60
+ * One line for a service whose own step genuinely redeploys when its
61
+ * recorded applied ref differs from the declared or pinned one (mail,
62
+ * archon, ai, files, integrations) -- `from` undefined (nothing applied
63
+ * yet, a first-ever apply) or already equal to `to` means nothing to
64
+ * report.
65
+ */
66
+ export declare function versionChangeLine(service: string, from: string | undefined, to: string, note: string): VersionChangeItem | null;
67
+ /**
68
+ * One line for a service whose own step never patches a running app's
69
+ * image tag once created -- zitadel, synapse, mas. #251 round 3: a prior
70
+ * attempt worded this line the same as {@link versionChangeLine}'s
71
+ * "X -> Y (image, redeploy)", which promised a redeploy `apply` can never
72
+ * actually perform for these three. `appLive` false means nothing live yet
73
+ * to compare against (no line, the same first-apply case
74
+ * {@link versionChangeLine} has); a live app with no recorded `applied`
75
+ * value predates this tracking and is reported as `running tag unknown`
76
+ * rather than silently showing nothing.
77
+ */
78
+ export declare function versionNotAppliedLine(service: string, appLive: boolean, applied: string | undefined, pinned: string): VersionChangeItem | null;
36
79
  /**
37
80
  * Scope keys (colon-free, so never mistaken for a `system:kind:key` resource
38
81
  * id) for foundry's ZITADEL admin machine user. The delivered key is the guard
@@ -75,6 +118,10 @@ export declare const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = "archon_applied_
75
118
  export declare const PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY = "archon_gate_closed";
76
119
  export declare const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = "zitadel_platform_admin_user_id";
77
120
  export declare const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = "zitadel_platform_admin_user_undelivered";
121
+ /** #283: the created instance's id, recorded once its owner token is delivered. */
122
+ export declare const PLATFORM_AUTH_INSTANCE_SCOPE_KEY = "zitadel_platform_instance_id";
123
+ /** #283: an instance that may exist without a delivered owner token; blocks creating another. */
124
+ export declare const PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY = "zitadel_platform_instance_undelivered";
78
125
  /**
79
126
  * #216 (building on #209/#215): the `IAM_LOGIN_CLIENT` machine user minted
80
127
  * for the login app and the broker, mirroring
@@ -269,24 +316,32 @@ export declare function platformMailMxHost(apex: string): string;
269
316
  export declare const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = "mail_applied_apex";
270
317
  export declare const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = "mail_applied_host";
271
318
  /**
272
- * #243: the org's `hosting.appEnv` values the last `coolify:app` apply pushed,
273
- * recorded as one JSON object (env name to value; ids and a domain, never a
274
- * secret). `planApp` plans the step again when {@link resolveAppEnv} differs
275
- * from it, so a changed map reaches an app that is already live.
319
+ * The key of an org's `app` hosting entry in a `coolify:app` or
320
+ * `cloudflare:access` step and in the ids recorded for it:
321
+ * `<orgId>:<entry key>`, so a second entry cannot collide with the first.
322
+ */
323
+ export declare function appStepKey(org: DeclaredOrg): string;
324
+ /**
325
+ * #243: the org's `app` entry's `env` values the last `coolify:app` apply
326
+ * pushed, recorded as one JSON object (env name to value; ids and a domain,
327
+ * never a secret). `planApp` plans the step again when {@link resolveAppEnv}
328
+ * differs from it, so a changed map reaches an app that is already live. The
329
+ * `.app` suffix is the entry key; a scope key holds no `:`, so
330
+ * `storeRecordedIds` still filters it out.
276
331
  */
277
- export declare const APP_ENV_APPLIED_SCOPE_KEY = "app_env_applied";
332
+ export declare const APP_ENV_APPLIED_SCOPE_KEY = "app_env_applied.app";
278
333
  /**
279
334
  * #239: set to '1' once an app has been restarted after its database URLs
280
335
  * were pushed. Not a URL. Without it a crash between the push and the restart
281
336
  * would leave a stale container that no plan notices.
282
337
  */
283
- export declare const DB_ENV_APPLIED_SCOPE_KEY = "db_env_applied";
338
+ export declare const DB_ENV_APPLIED_SCOPE_KEY = "db_env_applied.app";
284
339
  /**
285
- * The `hosting.appEnv` entries whose source has a recorded value, in env-name
286
- * order, and the env names whose source has none yet. Shared by `planApp` and
287
- * the executor's `pushAppEnv` so both read the same recorded ids.
340
+ * The entry's `env` values that have a value, in env-name order, and the env
341
+ * names whose source has none yet. Shared by `planApp` and the executor's
342
+ * `pushAppEnv` so both read the same recorded ids.
288
343
  */
289
- export declare function resolveAppEnv(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): {
344
+ export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>): {
290
345
  values: Record<string, string>;
291
346
  unresolved: Array<[string, string]>;
292
347
  };
@@ -441,6 +496,13 @@ export interface PlanPlatformOptions {
441
496
  ai?: string;
442
497
  archon?: string;
443
498
  mail?: string;
499
+ /**
500
+ * #257: true once a service's database exists but the host built the
501
+ * platform executor with no backup dependencies -- every such service's
502
+ * `coolify:backup:<service>` step is blocked, not planned, the same
503
+ * "executor missing" shape {@link authExecutorMissing} has for auth.
504
+ */
505
+ backupExecutorMissing?: boolean;
444
506
  };
445
507
  /**
446
508
  * Steps whose live state differs from the declared one in a way the recorded
@@ -486,6 +548,34 @@ export declare function planPlatform(platform: DeclaredPlatform, live: LiveResou
486
548
  * it, so the planner and the executor/reader can never key it apart).
487
549
  */
488
550
  export declare const JOBS_SCHEDULER_KEY = "jobs";
551
+ /**
552
+ * #257: the six platform databases foundry itself creates, each keyed by
553
+ * the scope id `platform-providers.ts`'s own `createPostgresDatabase` call
554
+ * site records once that service's database exists --
555
+ * `JOBS_DATABASE_SCOPE_KEY`, `PLATFORM_AUTH_DB_SCOPE_KEY`,
556
+ * `PLATFORM_ARCHON_DB_SCOPE_KEY`, `PLATFORM_AI_DB_SCOPE_KEY`,
557
+ * `PLATFORM_FILES_DB_SCOPE_KEY`, `INTEGRATIONS_DATABASE_SCOPE_KEY`. Those
558
+ * constants stay module-private in `platform-providers.ts`, grouped with
559
+ * each service's other scope keys; the string values here are a second,
560
+ * documented copy rather than an import, so `planBackups` below never
561
+ * reaches into that file and `platform-providers.ts` keeps importing *from*
562
+ * this one, never the other way (every other scope key constant already
563
+ * flows that direction). `platform-providers.test.ts`'s own
564
+ * `coolify:backup` suite exercises both the executor's real constants and
565
+ * this planner end to end, so a renamed key on either side shows up as a
566
+ * database the planner can no longer see -- reported blocked, per rule 2,
567
+ * never a wrong step.
568
+ */
569
+ export declare const PLATFORM_BACKUP_DATABASE_SCOPE_KEYS: {
570
+ readonly jobs: "coolify_jobs_database";
571
+ readonly auth: "coolify_platform_auth_database";
572
+ readonly archon: "coolify_platform_archon_database";
573
+ readonly ai: "coolify_platform_ai_database";
574
+ readonly files: "coolify_platform_files_database";
575
+ readonly integrations: "coolify_integrations_database";
576
+ };
577
+ export type PlatformBackupServiceKey = keyof typeof PLATFORM_BACKUP_DATABASE_SCOPE_KEYS;
578
+ export declare const PLATFORM_BACKUP_SERVICE_KEYS: readonly PlatformBackupServiceKey[];
489
579
  /**
490
580
  * #174: the resource key the integrations host is planned and recorded under,
491
581
  * `platform:integrations`, like {@link PLATFORM_AUTH_KEY}: the step is