@wtfalch/foundry 0.16.15 → 0.17.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 (64) hide show
  1. package/dist/apply.d.ts +22 -1
  2. package/dist/bootstrap/bootstrap.d.ts +6 -1
  3. package/dist/bootstrap/bootstrap.js +26 -3
  4. package/dist/bootstrap/declare.d.ts +11 -1
  5. package/dist/bootstrap/declare.js +12 -2
  6. package/dist/index.d.ts +9 -4
  7. package/dist/index.js +7 -0
  8. package/dist/migrations/0038_calls_version_pin.sql +8 -0
  9. package/dist/migrations/0039_destroy_record.sql +7 -0
  10. package/dist/migrations/0040_company_included_uuid.sql +72 -0
  11. package/dist/migrations/0041_company_included_uuid_keys.sql +132 -0
  12. package/dist/migrations/0042_mutation_leases.sql +12 -0
  13. package/dist/org.d.ts +82 -5
  14. package/dist/org.js +62 -1
  15. package/dist/plan/adopt.d.ts +76 -0
  16. package/dist/plan/adopt.js +184 -0
  17. package/dist/plan/agora.d.ts +21 -0
  18. package/dist/plan/agora.js +37 -0
  19. package/dist/plan/broker-entries.d.ts +9 -1
  20. package/dist/plan/broker-entries.js +13 -5
  21. package/dist/plan/chat-matrix.d.ts +1 -1
  22. package/dist/plan/chat-matrix.js +11 -3
  23. package/dist/plan/core-files.d.ts +25 -0
  24. package/dist/plan/core-files.js +25 -0
  25. package/dist/plan/deploy-health.d.ts +136 -0
  26. package/dist/plan/deploy-health.js +153 -0
  27. package/dist/plan/diff.d.ts +11 -3
  28. package/dist/plan/diff.js +77 -71
  29. package/dist/plan/entry-ai-key.d.ts +3 -3
  30. package/dist/plan/entry-files-key.d.ts +6 -4
  31. package/dist/plan/entry-files-key.js +7 -10
  32. package/dist/plan/executor.d.ts +109 -28
  33. package/dist/plan/executor.js +213 -56
  34. package/dist/plan/hostnames.d.ts +6 -0
  35. package/dist/plan/hostnames.js +13 -0
  36. package/dist/plan/index.d.ts +5 -1
  37. package/dist/plan/index.js +3 -1
  38. package/dist/plan/live.d.ts +5 -0
  39. package/dist/plan/login-host.d.ts +1 -1
  40. package/dist/plan/login-host.js +6 -5
  41. package/dist/plan/plan-apply.d.ts +153 -3
  42. package/dist/plan/plan-apply.js +584 -69
  43. package/dist/plan/platform-diff.d.ts +103 -9
  44. package/dist/plan/platform-diff.js +192 -14
  45. package/dist/plan/platform-org.d.ts +25 -0
  46. package/dist/plan/platform-org.js +17 -1
  47. package/dist/plan/platform-providers.d.ts +38 -5
  48. package/dist/plan/platform-providers.js +592 -109
  49. package/dist/plan/providers-readers.d.ts +2 -0
  50. package/dist/plan/providers-readers.js +4 -0
  51. package/dist/plan/rollout.d.ts +52 -0
  52. package/dist/plan/rollout.js +37 -0
  53. package/dist/plan/step-system.d.ts +22 -0
  54. package/dist/plan/step-system.js +37 -0
  55. package/dist/plan.d.ts +3 -0
  56. package/dist/state.d.ts +13 -0
  57. package/dist/store/audit.js +15 -1
  58. package/dist/store/postgres.d.ts +1 -1
  59. package/dist/store/postgres.js +127 -14
  60. package/dist/store/schema.d.ts +134 -5
  61. package/dist/store/schema.js +12 -1
  62. package/dist/store.d.ts +47 -4
  63. package/dist/store.js +9 -1
  64. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
1
+ import type { DeclaredOrg, EnvSharedSecret, HostingEntry, OrgId } from '../org.js';
2
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';
@@ -107,6 +107,17 @@ export declare const PLATFORM_ARCHON_KEY = "platform:archon";
107
107
  */
108
108
  export declare const PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY = "archon_applied_hostname";
109
109
  export declare const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = "archon_applied_git_ref";
110
+ /**
111
+ * #398: the integrations management key id Archon runs with, recorded by
112
+ * `provisionPlatformArchon` only after its deployment finished and its health
113
+ * check passed with that id in its env. `provisionPlatformIntegrations`'s
114
+ * rotation step reads this before dropping a previous key id from the
115
+ * runner's `MANAGEMENT_PUBLIC_KEYS`: the old key stays pinned until this
116
+ * matches the new id, so a request signed by an Archon container still
117
+ * running the old key is never refused. If Archon's deploy fails this stays
118
+ * unrecorded and the old key stays pinned.
119
+ */
120
+ export declare const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = "archon_integrations_key_synced";
110
121
  /**
111
122
  * #211 round 4: the broker app id Archon last registered its entry with.
112
123
  * `provisionPlatformArchon` reads the broker to register with from
@@ -118,6 +129,19 @@ export declare const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = "archon_applied
118
129
  * changed hostname or `gitRef` does.
119
130
  */
120
131
  export declare const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = "archon_applied_broker_app";
132
+ /**
133
+ * The `AUTHZ_BOOTSTRAP_OPERATOR` the last Archon apply set (`none` when it set
134
+ * none). `planArchon` plans the step again when the owner's id recorded by
135
+ * `platform:auth` differs from it, so Archon picks the owner up once auth
136
+ * has recorded it.
137
+ */
138
+ export declare const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = "archon_applied_bootstrap_operator";
139
+ /**
140
+ * The owner's ZITADEL user id Archon takes as its first operator, from
141
+ * {@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY}. Undefined when none is
142
+ * recorded or the recorded value is `none` (no owner declared).
143
+ */
144
+ export declare function archonBootstrapOperator(recorded: Readonly<Record<string, string>>): string | undefined;
121
145
  /**
122
146
  * #175: set to '1' before the step points the tunnel at `http_status:404`
123
147
  * (Access app rebuild), cleared to '' once the real ingress is written back.
@@ -285,6 +309,20 @@ export declare const PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY = "coolify_platform
285
309
  export declare const PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF = "platform:master:coolify-url";
286
310
  export declare const PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF = "platform:master:coolify-token";
287
311
  export declare const PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF = "platform:master:cloudflare-token";
312
+ /**
313
+ * #436: a Coolify API token scoped to `read` only, for Archon's own
314
+ * `/backups` page (`COOLIFY_URL`/`COOLIFY_READ_TOKEN`, Archon's
315
+ * `src/lib/backups/coolify.ts`) -- deliberately not
316
+ * {@link PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF}'s root token, which a
317
+ * read-only status page must never hold. Coolify's token UI has no API
318
+ * counterpart to create one (`@wtfalch/providers`'s `CoolifyClient` doc:
319
+ * "Keys & Tokens -> API tokens" only), so a human creates it in Coolify's
320
+ * console and holds it under this ref; `provisionPlatformArchon` only reads
321
+ * it back, and pushes nothing until it is held. `COOLIFY_URL` needs no ref
322
+ * of its own: it is not secret, and {@link PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF}
323
+ * already holds the same panel's address.
324
+ */
325
+ export declare const PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF = "platform:archon:coolify-read-token";
288
326
  /**
289
327
  * #169: `platform:<service>` for the platform's own mail server (Stalwart),
290
328
  * mirroring {@link PLATFORM_AUTH_KEY} exactly.
@@ -400,14 +438,31 @@ export declare function appDomainsAppliedScopeKey(entryKey: string): string;
400
438
  * entry), so an app that runs today plans no change.
401
439
  */
402
440
  export declare function appliedAppDomains(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, entryKey: string): string[];
441
+ /** Whether the secret sources can be filled right now: the mail key is held, and a new shared secret can be held. */
442
+ export interface AppSecretAvailability {
443
+ platformMailKey: boolean;
444
+ sharedSecret: boolean;
445
+ }
403
446
  /**
404
447
  * The entry's `env` values that have a value, in env-name order, and the env
405
448
  * names whose source has none yet. Shared by `planApp` and the executor's
406
449
  * `pushAppEnv` so both read the same recorded ids.
450
+ *
451
+ * A secret source is never a value here. `secrets` names each one that can be
452
+ * filled (`available`), `unresolvedSecrets` each that cannot, and `marker`
453
+ * is `values` plus a `secret:<source>` label per available secret: the JSON
454
+ * `coolifyApp` records as applied, so a declared change replans and no secret
455
+ * is written into it.
407
456
  */
408
- export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>): {
457
+ export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>, available?: AppSecretAvailability): {
409
458
  values: Record<string, string>;
410
459
  unresolved: Array<[string, string]>;
460
+ secrets: Array<{
461
+ name: string;
462
+ source: 'platformMailKey' | EnvSharedSecret;
463
+ }>;
464
+ unresolvedSecrets: Array<[string, string]>;
465
+ marker: Record<string, string>;
411
466
  };
412
467
  /**
413
468
  * #172: the resource key `planAi` plans under and `FoundryPlatformStepExecutor`
@@ -478,13 +533,6 @@ export declare const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = "ai_platform_migrat
478
533
  * that throws) is repaired by the next one.
479
534
  */
480
535
  export declare const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = "ai_platform_deploy_pending";
481
- /**
482
- * The Coolify deployment queued for the host (its `deployment_uuid`, or
483
- * `untracked` when the deploy answered none), set when that deploy is queued
484
- * and cleared once it finished. The mint waits for it, so a retry after a
485
- * failed wait still waits instead of minting against the old container.
486
- */
487
- export declare const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = "ai_platform_runner_deployment";
488
536
  /**
489
537
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
490
538
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -684,3 +732,49 @@ export declare const PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY = "integrations_man
684
732
  /** The hostname and gitRef the last successful apply put live; a difference from the declared ones plans the step again. */
685
733
  export declare const PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY = "integrations_applied_hostname";
686
734
  export declare const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = "integrations_applied_gitref";
735
+ /**
736
+ * #398: the key id currently pinned on the runner as the active entry of
737
+ * `MANAGEMENT_PUBLIC_KEYS` -- `foundry-1` on the first mint, and whatever a
738
+ * rotation minted next after that. `collectArchonServiceInputs` reads this
739
+ * (falling back to the fixed `INTEGRATIONS_MANAGEMENT_KEY_ID` constant for a
740
+ * platform applied before this key existed) for Archon's own
741
+ * `INTEGRATIONS_MANAGEMENT_KEY_ID` env, so a rotated id reaches it without
742
+ * Archon's step needing to know anything happened on the integrations side.
743
+ */
744
+ export declare const PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY = "integrations_management_key_id";
745
+ /**
746
+ * #398: the key id the runner was last deployed with in its
747
+ * `MANAGEMENT_PUBLIC_KEYS`, recorded by `provisionPlatformIntegrations` only
748
+ * after the runner's deployment finished. Archon is not moved to a new key id
749
+ * until this equals the active id, so Archon never signs with a key the
750
+ * running runner does not yet hold.
751
+ */
752
+ export declare const PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY = "integrations_runner_keyring_deployed";
753
+ /**
754
+ * #398: the same two-key discipline as
755
+ * {@link PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY}, for a rotation's
756
+ * new pair: recorded the moment it is minted, before delivery, and cleared
757
+ * by nothing -- {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} moving past it
758
+ * is what later runs read as "delivered and pinned", the same way the
759
+ * once-only mint's guard is a second key rather than removing the first.
760
+ * An id here that {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} has not
761
+ * caught up to means the pair may be lost or half-applied; the step reports
762
+ * it and mints no further pair.
763
+ */
764
+ export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY = "integrations_management_key_rotation_undelivered";
765
+ /**
766
+ * #398: the previous key id, still pinned in `MANAGEMENT_PUBLIC_KEYS` beside
767
+ * the new one, pending removal -- set the moment the new pair is pinned
768
+ * beside it, and cleared (to `''`, the {@link PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY}
769
+ * convention for "was set, now isn't") once the previous id is actually
770
+ * removed from the keyring. Its presence is the overlap window: both keys
771
+ * verify a request while it holds.
772
+ */
773
+ export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = "integrations_management_key_rotation_previous";
774
+ /**
775
+ * #398: how a rotation is requested. A person records any non-empty value
776
+ * under this scope key; `planIntegrations` then plans the step again and
777
+ * `provisionPlatformIntegrations` mints the new pair, clearing the key (to
778
+ * `''`) as soon as the mint is recorded, so one request is one rotation.
779
+ */
780
+ export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = "integrations_management_key_rotation_requested";
@@ -2,6 +2,7 @@ import { VERSIONS } from '../bootstrap/versions.js';
2
2
  import { APP_ENTRY_KEY } from '../org.js';
3
3
  import { VERSION_CHANGE_KIND, } from '../plan.js';
4
4
  import { legacyAppHostname } from './hostnames.js';
5
+ import { withStepSystem } from './step-system.js';
5
6
  /**
6
7
  * The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
7
8
  * mirroring `diff.ts`'s `toPlan`. There is no separate platform-shaped `Plan`
@@ -127,6 +128,17 @@ export const PLATFORM_ARCHON_KEY = 'platform:archon';
127
128
  */
128
129
  export const PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY = 'archon_applied_hostname';
129
130
  export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref';
131
+ /**
132
+ * #398: the integrations management key id Archon runs with, recorded by
133
+ * `provisionPlatformArchon` only after its deployment finished and its health
134
+ * check passed with that id in its env. `provisionPlatformIntegrations`'s
135
+ * rotation step reads this before dropping a previous key id from the
136
+ * runner's `MANAGEMENT_PUBLIC_KEYS`: the old key stays pinned until this
137
+ * matches the new id, so a request signed by an Archon container still
138
+ * running the old key is never refused. If Archon's deploy fails this stays
139
+ * unrecorded and the old key stays pinned.
140
+ */
141
+ export const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = 'archon_integrations_key_synced';
130
142
  /**
131
143
  * #211 round 4: the broker app id Archon last registered its entry with.
132
144
  * `provisionPlatformArchon` reads the broker to register with from
@@ -138,6 +150,22 @@ export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref
138
150
  * changed hostname or `gitRef` does.
139
151
  */
140
152
  export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_app';
153
+ /**
154
+ * The `AUTHZ_BOOTSTRAP_OPERATOR` the last Archon apply set (`none` when it set
155
+ * none). `planArchon` plans the step again when the owner's id recorded by
156
+ * `platform:auth` differs from it, so Archon picks the owner up once auth
157
+ * has recorded it.
158
+ */
159
+ export const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = 'archon_applied_bootstrap_operator';
160
+ /**
161
+ * The owner's ZITADEL user id Archon takes as its first operator, from
162
+ * {@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY}. Undefined when none is
163
+ * recorded or the recorded value is `none` (no owner declared).
164
+ */
165
+ export function archonBootstrapOperator(recorded) {
166
+ const id = recorded[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
167
+ return id && id !== 'none' ? id : undefined;
168
+ }
141
169
  /**
142
170
  * #175: set to '1' before the step points the tunnel at `http_status:404`
143
171
  * (Access app rebuild), cleared to '' once the real ingress is written back.
@@ -316,6 +344,20 @@ export const PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY = 'coolify_platform_archon_
316
344
  export const PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF = 'platform:master:coolify-url';
317
345
  export const PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF = 'platform:master:coolify-token';
318
346
  export const PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF = 'platform:master:cloudflare-token';
347
+ /**
348
+ * #436: a Coolify API token scoped to `read` only, for Archon's own
349
+ * `/backups` page (`COOLIFY_URL`/`COOLIFY_READ_TOKEN`, Archon's
350
+ * `src/lib/backups/coolify.ts`) -- deliberately not
351
+ * {@link PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF}'s root token, which a
352
+ * read-only status page must never hold. Coolify's token UI has no API
353
+ * counterpart to create one (`@wtfalch/providers`'s `CoolifyClient` doc:
354
+ * "Keys & Tokens -> API tokens" only), so a human creates it in Coolify's
355
+ * console and holds it under this ref; `provisionPlatformArchon` only reads
356
+ * it back, and pushes nothing until it is held. `COOLIFY_URL` needs no ref
357
+ * of its own: it is not secret, and {@link PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF}
358
+ * already holds the same panel's address.
359
+ */
360
+ export const PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF = 'platform:archon:coolify-read-token';
319
361
  /**
320
362
  * #169: `platform:<service>` for the platform's own mail server (Stalwart),
321
363
  * mirroring {@link PLATFORM_AUTH_KEY} exactly.
@@ -448,8 +490,14 @@ export function appliedAppDomains(org, recorded, entryKey) {
448
490
  * The entry's `env` values that have a value, in env-name order, and the env
449
491
  * names whose source has none yet. Shared by `planApp` and the executor's
450
492
  * `pushAppEnv` so both read the same recorded ids.
493
+ *
494
+ * A secret source is never a value here. `secrets` names each one that can be
495
+ * filled (`available`), `unresolvedSecrets` each that cannot, and `marker`
496
+ * is `values` plus a `secret:<source>` label per available secret: the JSON
497
+ * `coolifyApp` records as applied, so a declared change replans and no secret
498
+ * is written into it.
451
499
  */
452
- export function resolveAppEnv(org, entry, recorded) {
500
+ export function resolveAppEnv(org, entry, recorded, available = { platformMailKey: false, sharedSecret: false }) {
453
501
  const sources = {
454
502
  filesOrganisationId: recorded[`files:organisation:${org.id}`],
455
503
  aiOrganisationId: recorded[`ai:organisation:${org.id}`],
@@ -457,14 +505,38 @@ export function resolveAppEnv(org, entry, recorded) {
457
505
  };
458
506
  const values = {};
459
507
  const unresolved = [];
508
+ const secrets = [];
509
+ const unresolvedSecrets = [];
510
+ const marker = {};
460
511
  for (const [name, declared] of Object.entries(entry.env).sort(([a], [b]) => (a < b ? -1 : 1))) {
512
+ if (typeof declared !== 'string' && declared.from === 'platformMailKey') {
513
+ if (available.platformMailKey) {
514
+ secrets.push({ name, source: 'platformMailKey' });
515
+ marker[name] = 'secret:platformMailKey';
516
+ }
517
+ else
518
+ unresolvedSecrets.push([name, 'platformMailKey']);
519
+ continue;
520
+ }
521
+ if (typeof declared !== 'string' && declared.from === 'sharedSecret') {
522
+ const label = `sharedSecret ${declared.name}`;
523
+ if (available.sharedSecret) {
524
+ secrets.push({ name, source: declared });
525
+ marker[name] = `secret:sharedSecret:${declared.name}`;
526
+ }
527
+ else
528
+ unresolvedSecrets.push([name, label]);
529
+ continue;
530
+ }
461
531
  const value = typeof declared === 'string' ? declared : sources[declared.from];
462
- if (value)
532
+ if (value) {
463
533
  values[name] = value;
534
+ marker[name] = value;
535
+ }
464
536
  else if (typeof declared !== 'string')
465
537
  unresolved.push([name, declared.from]);
466
538
  }
467
- return { values, unresolved };
539
+ return { values, unresolved, secrets, unresolvedSecrets, marker };
468
540
  }
469
541
  /**
470
542
  * #172: the resource key `planAi` plans under and `FoundryPlatformStepExecutor`
@@ -535,13 +607,6 @@ export const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = 'ai_platform_migrate_blocke
535
607
  * that throws) is repaired by the next one.
536
608
  */
537
609
  export const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = 'ai_platform_deploy_pending';
538
- /**
539
- * The Coolify deployment queued for the host (its `deployment_uuid`, or
540
- * `untracked` when the deploy answered none), set when that deploy is queued
541
- * and cleared once it finished. The mint waits for it, so a retry after a
542
- * failed wait still waits instead of minting against the old container.
543
- */
544
- export const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = 'ai_platform_runner_deployment';
545
610
  /**
546
611
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
547
612
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -715,8 +780,19 @@ export function planPlatform(platform, live, options = {}) {
715
780
  unmanaged.push({ description: ai.blocked, key: 'ai' });
716
781
  if (archon.blocked)
717
782
  unmanaged.push({ description: archon.blocked, key: 'archon' });
783
+ // #398: the old integrations key stays pinned until Archon holds the new
784
+ // id; name that when Archon is what is stopping it.
785
+ if (archon.blocked &&
786
+ recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY] &&
787
+ recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] !==
788
+ recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY]) {
789
+ unmanaged.push({
790
+ description: `the previous integrations management key ${recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY]} stays pinned on the runner until Archon is redeployed with the new key, and Archon is blocked (see above)`,
791
+ key: 'integrations',
792
+ });
793
+ }
718
794
  unmanaged.push(...backups.blocked);
719
- return { plannedSteps, unmanaged, versionChanges };
795
+ return { plannedSteps: plannedSteps.map(withStepSystem), unmanaged, versionChanges };
720
796
  }
721
797
  /**
722
798
  * The platform's own Cloudflare zone for its shared apex (`platform.ts`'s
@@ -1344,6 +1420,52 @@ export const PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY = 'integrations_management_
1344
1420
  /** The hostname and gitRef the last successful apply put live; a difference from the declared ones plans the step again. */
1345
1421
  export const PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY = 'integrations_applied_hostname';
1346
1422
  export const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = 'integrations_applied_gitref';
1423
+ /**
1424
+ * #398: the key id currently pinned on the runner as the active entry of
1425
+ * `MANAGEMENT_PUBLIC_KEYS` -- `foundry-1` on the first mint, and whatever a
1426
+ * rotation minted next after that. `collectArchonServiceInputs` reads this
1427
+ * (falling back to the fixed `INTEGRATIONS_MANAGEMENT_KEY_ID` constant for a
1428
+ * platform applied before this key existed) for Archon's own
1429
+ * `INTEGRATIONS_MANAGEMENT_KEY_ID` env, so a rotated id reaches it without
1430
+ * Archon's step needing to know anything happened on the integrations side.
1431
+ */
1432
+ export const PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY = 'integrations_management_key_id';
1433
+ /**
1434
+ * #398: the key id the runner was last deployed with in its
1435
+ * `MANAGEMENT_PUBLIC_KEYS`, recorded by `provisionPlatformIntegrations` only
1436
+ * after the runner's deployment finished. Archon is not moved to a new key id
1437
+ * until this equals the active id, so Archon never signs with a key the
1438
+ * running runner does not yet hold.
1439
+ */
1440
+ export const PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY = 'integrations_runner_keyring_deployed';
1441
+ /**
1442
+ * #398: the same two-key discipline as
1443
+ * {@link PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY}, for a rotation's
1444
+ * new pair: recorded the moment it is minted, before delivery, and cleared
1445
+ * by nothing -- {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} moving past it
1446
+ * is what later runs read as "delivered and pinned", the same way the
1447
+ * once-only mint's guard is a second key rather than removing the first.
1448
+ * An id here that {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} has not
1449
+ * caught up to means the pair may be lost or half-applied; the step reports
1450
+ * it and mints no further pair.
1451
+ */
1452
+ export const PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY = 'integrations_management_key_rotation_undelivered';
1453
+ /**
1454
+ * #398: the previous key id, still pinned in `MANAGEMENT_PUBLIC_KEYS` beside
1455
+ * the new one, pending removal -- set the moment the new pair is pinned
1456
+ * beside it, and cleared (to `''`, the {@link PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY}
1457
+ * convention for "was set, now isn't") once the previous id is actually
1458
+ * removed from the keyring. Its presence is the overlap window: both keys
1459
+ * verify a request while it holds.
1460
+ */
1461
+ export const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = 'integrations_management_key_rotation_previous';
1462
+ /**
1463
+ * #398: how a rotation is requested. A person records any non-empty value
1464
+ * under this scope key; `planIntegrations` then plans the step again and
1465
+ * `provisionPlatformIntegrations` mints the new pair, clearing the key (to
1466
+ * `''`) as soon as the mint is recorded, so one request is one rotation.
1467
+ */
1468
+ export const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = 'integrations_management_key_rotation_requested';
1347
1469
  /**
1348
1470
  * #174: the integrations host (https://github.com/wtfalch/integrations) -- its
1349
1471
  * migrate job, web app and queue worker, their shared database, and its
@@ -1360,6 +1482,13 @@ export const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = 'integrations_appl
1360
1482
  * Blocked, not planned: an undelivered key, and an executor that cannot run
1361
1483
  * the step (no integrations dependencies, or no `@wtfalch/keys` values for
1362
1484
  * the host).
1485
+ *
1486
+ * #398: a rotation (see {@link PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY})
1487
+ * or one still pending removal of the previous key
1488
+ * ({@link PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY}) plans the
1489
+ * step again even when `applied`. A rotation whose new pair is minted but
1490
+ * not confirmed delivered is blocked, never planned: the step would throw
1491
+ * on every apply.
1363
1492
  */
1364
1493
  function planIntegrations(platform, recordedIds, executorBlocked) {
1365
1494
  if (!platform.integrations.enabled)
@@ -1368,7 +1497,41 @@ function planIntegrations(platform, recordedIds, executorBlocked) {
1368
1497
  const applied = Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_INTEGRATIONS_KEY)]) &&
1369
1498
  recordedIds[PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY] === hostname &&
1370
1499
  recordedIds[PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY] === gitRef;
1371
- if (applied)
1500
+ const keyId = recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY];
1501
+ const previous = recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY];
1502
+ const keySet = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY]);
1503
+ // #398: a rotation key minted but not confirmed delivered and pinned blocks
1504
+ // the step whatever else is recorded: the executor throws on it, and
1505
+ // Archon's env refuses to pair the held key with an older id.
1506
+ const rotationUndelivered = recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY];
1507
+ if (rotationUndelivered && rotationUndelivered !== keyId) {
1508
+ return {
1509
+ step: null,
1510
+ blocked: `the integrations host is blocked, not planned: rotation key ${rotationUndelivered} was minted but not confirmed delivered and pinned; check the held key and the runner's MANAGEMENT_PUBLIC_KEYS, then remove the recorded key ${PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY} and set ${PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY} again to mint a new one`,
1511
+ };
1512
+ }
1513
+ // A request counts only once the first key is minted and no earlier
1514
+ // rotation is waiting to drop its previous key.
1515
+ const requested = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY]) &&
1516
+ keySet &&
1517
+ !previous;
1518
+ const requestIgnored = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY]) && !requested;
1519
+ // The previous key is dropped once Archon holds the new id, or at once when
1520
+ // Archon is not enabled (nothing else holds the old key). While Archon has
1521
+ // not synced there is nothing for this step to do.
1522
+ // Mirrors the executor's drop guards: the previous id is not the active
1523
+ // one, and the last delivered mint is the active one.
1524
+ const dropReady = Boolean(previous) &&
1525
+ previous !== keyId &&
1526
+ rotationUndelivered === keyId &&
1527
+ (!platform.archon.enabled ||
1528
+ recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY] === keyId);
1529
+ // The runner has not been deployed with the active key id yet (a deploy
1530
+ // failed after the key was pinned): plan the step to deploy it.
1531
+ const runnerStale = Boolean(keyId) &&
1532
+ recordedIds[PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY] !== keyId;
1533
+ const rotating = requested || dropReady || runnerStale;
1534
+ if (applied && !rotating)
1372
1535
  return { step: null, blocked: null };
1373
1536
  const undelivered = recordedIds[PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY];
1374
1537
  if (undelivered && !recordedIds[PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY]) {
@@ -1388,7 +1551,9 @@ function planIntegrations(platform, recordedIds, executorBlocked) {
1388
1551
  desired: { hostname, gitRef },
1389
1552
  },
1390
1553
  step: {
1391
- description: `provision the integrations host at ${hostname}`,
1554
+ description: `provision the integrations host at ${hostname}${requestIgnored
1555
+ ? `; the recorded rotation request ${PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY} is ignored and cleared (no key minted yet, or the previous key ${previous ?? ''} is still pending removal); set it again afterwards`
1556
+ : ''}`,
1392
1557
  destroys: false,
1393
1558
  migrates: false,
1394
1559
  },
@@ -1613,7 +1778,20 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted)
1613
1778
  Boolean(recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY]) &&
1614
1779
  recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ===
1615
1780
  recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY] &&
1616
- !recordedIds[PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY];
1781
+ (recordedIds[PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY] ?? 'none') ===
1782
+ (archonBootstrapOperator(recordedIds) ?? 'none') &&
1783
+ !recordedIds[PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY] &&
1784
+ // #398: a rotated integrations key id Archon's env was not yet written
1785
+ // with (see PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY). Absent
1786
+ // before the first rotation on a platform applied before #398.
1787
+ // Only while integrations is enabled, and only once the runner is deployed
1788
+ // with that id (PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY).
1789
+ (!platform.integrations.enabled ||
1790
+ !recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ||
1791
+ recordedIds[PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY] !==
1792
+ recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ||
1793
+ recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ===
1794
+ recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY]);
1617
1795
  if (applied && !drifted)
1618
1796
  return { step: null, blocked: null };
1619
1797
  if (!platform.auth.enabled) {
@@ -1,5 +1,21 @@
1
1
  import type { DeclaredPlatform } from '../platform.js';
2
2
  import type { AgoraHostValues } from './agora.js';
3
+ /**
4
+ * #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
5
+ * fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
6
+ * `bouleHostnameFor` and `hostnames.ts`'s own `<slug>.oikos.host` already
7
+ * use for a level-1 org's hostnames. `OrgPlatformReadiness.mailHost.apex`
8
+ * used to be `platform.apex` (the platform's own brand apex, e.g.
9
+ * `companyincluded.com`), which gave a domainless org `slug@companyincluded.com`
10
+ * instead of the settled `slug@oikos.host` contract
11
+ * (`@wtfalch/email`'s `org-mail.ts`'s `resolveDomain` is the only reader of
12
+ * this `apex`, for a level-1 org alone -- a with-domain org returns
13
+ * `org.domain` and never looks at it). `mailHost` itself (the MX server's own
14
+ * hostname, `platformMailMxHost(platform.apex)`) is unaffected: the shared
15
+ * mail server stays the platform's own, whichever apex a mailbox's address
16
+ * uses.
17
+ */
18
+ export declare const NO_DOMAIN_MAIL_APEX = "oikos.host";
3
19
  /**
4
20
  * https://github.com/wtfalch/foundry/issues/177: what an org step needs to
5
21
  * know about the platform it runs on -- whether each shared service it
@@ -68,6 +84,13 @@ export interface OrgPlatformReadiness {
68
84
  ai: string | null;
69
85
  jobs: string | null;
70
86
  };
87
+ /**
88
+ * The platform's Lettermint key (`PLATFORM_AUTH_BROKER_MAIL_KEY_REF`) is held,
89
+ * so an entry env from `platformMailKey` can be filled. Absent reads as not held.
90
+ */
91
+ mailKeyHeld?: boolean;
92
+ /** The host's held keys can `put`, so an entry env from `sharedSecret` can be filled. Absent reads as false. */
93
+ canHoldSecrets?: boolean;
71
94
  /**
72
95
  * True when the host wired no held-keys reader, so no key could be checked.
73
96
  * A missing key is then reported as "no reader given" instead of "not held".
@@ -91,6 +114,8 @@ export declare const ORG_PLATFORM_KEY_REFS: {
91
114
  /** The one method of Archon's held keys this package reads. A value is returned to the caller and never logged, recorded or put in a plan. */
92
115
  export interface HeldKeyResolver {
93
116
  resolve(ref: string): Promise<string | null | undefined>;
117
+ /** Holds a value under `ref`; only the org steps' shared secrets need it. */
118
+ put?(ref: string, value: string): Promise<void>;
94
119
  }
95
120
  /**
96
121
  * Which of {@link ORG_PLATFORM_KEY_REFS} `heldKeys` can resolve right now. A
@@ -1,5 +1,21 @@
1
1
  import { JOBS_SCHEDULER_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
2
2
  import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './platform-providers.js';
3
+ /**
4
+ * #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
5
+ * fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
6
+ * `bouleHostnameFor` and `hostnames.ts`'s own `<slug>.oikos.host` already
7
+ * use for a level-1 org's hostnames. `OrgPlatformReadiness.mailHost.apex`
8
+ * used to be `platform.apex` (the platform's own brand apex, e.g.
9
+ * `companyincluded.com`), which gave a domainless org `slug@companyincluded.com`
10
+ * instead of the settled `slug@oikos.host` contract
11
+ * (`@wtfalch/email`'s `org-mail.ts`'s `resolveDomain` is the only reader of
12
+ * this `apex`, for a level-1 org alone -- a with-domain org returns
13
+ * `org.domain` and never looks at it). `mailHost` itself (the MX server's own
14
+ * hostname, `platformMailMxHost(platform.apex)`) is unaffected: the shared
15
+ * mail server stays the platform's own, whichever apex a mailbox's address
16
+ * uses.
17
+ */
18
+ export const NO_DOMAIN_MAIL_APEX = 'oikos.host';
3
19
  /**
4
20
  * What an org's plan reads when no platform is declared: nothing is ready, so
5
21
  * every org step that depends on a platform service is blocked (#177). A host
@@ -73,7 +89,7 @@ noKeyReader = false) {
73
89
  };
74
90
  return {
75
91
  mailHost: mailApplied
76
- ? { apex: platform.apex, mailHost: platformMailMxHost(platform.apex) }
92
+ ? { apex: NO_DOMAIN_MAIL_APEX, mailHost: platformMailMxHost(platform.apex) }
77
93
  : null,
78
94
  filesReady: filesApplied && missingKeys.files === null,
79
95
  filesUrl: filesApplied
@@ -1,4 +1,5 @@
1
1
  import type { CloudflareClient } from '@wtfalch/providers/cloudflare';
2
+ import type { DeclaredOrg } from '../org.js';
2
3
  import type { DeclaredPlatform } from '../platform.js';
3
4
  import type { DeclaredOrgStore, PlatformStore } from '../store.js';
4
5
  import type { StepAction } from './diff.js';
@@ -243,6 +244,13 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
243
244
  backupStorage?: boolean;
244
245
  backupStorageBlocked?: string;
245
246
  }>;
247
+ /**
248
+ * Gives the files runner a changed share-domain set and nothing else: its
249
+ * Coolify `domains`, `FILES_PUBLIC_HOSTS` and one redeploy of the runner.
250
+ * Returns the scope ids to record, or `null` when nothing changed
251
+ * (or the runner was never applied).
252
+ */
253
+ refreshFilesShare(_platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<Record<string, string> | null>;
246
254
  execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
247
255
  resourceId?: string;
248
256
  } | undefined>;
@@ -357,7 +365,7 @@ export declare function defaultStalwartForInstance(input: {
357
365
  * own shape.
358
366
  */
359
367
  export interface PlatformMailDeps {
360
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
368
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
361
369
  cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'>;
362
370
  /**
363
371
  * Must target the platform's own ZITADEL instance, or `zitadelForInstance`
@@ -401,7 +409,7 @@ export interface PlatformMailRuntime {
401
409
  * {@link PlatformStepExecutionContext}, bundled like {@link PlatformAuthDeps}.
402
410
  */
403
411
  export interface PlatformArchonDeps {
404
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
412
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'deleteEnv' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
405
413
  cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord' | 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
406
414
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
407
415
  zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp'>;
@@ -447,6 +455,17 @@ export interface PlatformArchonDeps {
447
455
  coolifyToken: string;
448
456
  cloudflareParentToken: string;
449
457
  };
458
+ /** Clock, sleep and fetch for the post-deploy health wait (#252); real ones by default. */
459
+ runtime?: PlatformArchonRuntime;
460
+ }
461
+ export interface PlatformArchonRuntime {
462
+ fetch?: typeof fetch;
463
+ sleep?: (ms: number) => Promise<void>;
464
+ now?: () => number;
465
+ /** How long to wait for `/api/health` to answer the new deploy (default 5 minutes). */
466
+ readyTimeoutMs?: number;
467
+ /** Pause between readiness polls (default 5 seconds). */
468
+ readyIntervalMs?: number;
450
469
  }
451
470
  /**
452
471
  * The name of foundry's own Cloudflare Access app in front of Archon. Same
@@ -562,7 +581,7 @@ export declare class AiManagementKeyUntrustedError extends Error {
562
581
  }
563
582
  /** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
564
583
  export interface PlatformAiDeps {
565
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
584
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
566
585
  deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
567
586
  /**
568
587
  * Reads back a secret this step delivered on an earlier apply -- needed
@@ -690,7 +709,7 @@ export declare function brokerDeployRecordValue(brokerAppId: string, authAppsRaw
690
709
  * {@link PlatformAuthDeps}.
691
710
  */
692
711
  export interface PlatformFilesDeps {
693
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
712
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
694
713
  /**
695
714
  * Must target the platform's own sign-in (the instance `platform:auth`
696
715
  * built), or {@link zitadelForInstance} must be set.
@@ -799,7 +818,7 @@ export declare const FILES_INTROSPECTION_SECRET_REF = "zitadel:platform:files:in
799
818
  * {@link FoundryPlatformStepExecutor} keeps compiling with it omitted.
800
819
  */
801
820
  export interface PlatformIntegrationsDeps {
802
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'raw'>;
821
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'deployAndWait' | 'raw'>;
803
822
  /** Hands the management private key to the host, once (`FoundryStepExecutorPorts.deliverSecret`'s contract). */
804
823
  deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
805
824
  /**
@@ -898,4 +917,18 @@ export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyRe
898
917
  constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
899
918
  listResources(): Promise<LiveResource[]>;
900
919
  }
920
+ /** Scope id (no `:`): the share domains the runner's Coolify `domains` was last set to, comma-joined. */
921
+ export declare const PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY = "platform_files_share_domains";
922
+ /**
923
+ * The runner's `FILES_PUBLIC_HOSTS`: the host-supplied `publicHosts` first, in
924
+ * their given order, then the share domains sorted, with no host twice.
925
+ * Empty when there is neither.
926
+ */
927
+ export declare function filesPublicHostsValue(publicHosts: string | undefined, shareDomains: readonly string[] | undefined): string;
928
+ /**
929
+ * The share domain of every org that has a domain and an entry declaring
930
+ * `uses.files.shareHost`: `share.<domain>`. The platform files step derives it
931
+ * from the declared orgs, for its apply and its drift check alike.
932
+ */
933
+ export declare function filesShareDomains(orgs: readonly DeclaredOrg[]): string[];
901
934
  export {};