@wtfalch/foundry 0.34.0 → 0.35.2

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 (88) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +1 -1
  2. package/dist/bootstrap/bootstrap.js +49 -11
  3. package/dist/bootstrap/declare.d.ts +12 -5
  4. package/dist/bootstrap/declare.js +33 -18
  5. package/dist/bootstrap/versions.d.ts +20 -0
  6. package/dist/bootstrap/versions.js +46 -0
  7. package/dist/calls-sfu.js +3 -3
  8. package/dist/index.d.ts +9 -4
  9. package/dist/index.js +5 -2
  10. package/dist/migrations/0053_heartbeats_provider.sql +9 -0
  11. package/dist/migrations/0053_org_version_pin.sql +6 -0
  12. package/dist/org.d.ts +60 -14
  13. package/dist/org.js +118 -28
  14. package/dist/plan/backup-gate.d.ts +93 -0
  15. package/dist/plan/backup-gate.js +261 -0
  16. package/dist/plan/broker-entries.d.ts +9 -1
  17. package/dist/plan/broker-entries.js +20 -3
  18. package/dist/plan/build-record.d.ts +56 -0
  19. package/dist/plan/build-record.js +109 -0
  20. package/dist/plan/calls-matrix.js +3 -3
  21. package/dist/plan/cloudflared-connector.js +2 -2
  22. package/dist/plan/company-backups.d.ts +31 -0
  23. package/dist/plan/company-backups.js +101 -0
  24. package/dist/plan/deploy-health.d.ts +12 -2
  25. package/dist/plan/deploy-health.js +3 -1
  26. package/dist/plan/destinations.d.ts +73 -0
  27. package/dist/plan/destinations.js +217 -0
  28. package/dist/plan/diff.d.ts +17 -8
  29. package/dist/plan/diff.js +131 -131
  30. package/dist/plan/edge-access.d.ts +15 -0
  31. package/dist/plan/edge-access.js +21 -2
  32. package/dist/plan/executor.d.ts +166 -7
  33. package/dist/plan/executor.js +547 -106
  34. package/dist/plan/health-targets.d.ts +1 -3
  35. package/dist/plan/health-targets.js +36 -36
  36. package/dist/plan/held-key-refs.d.ts +5 -0
  37. package/dist/plan/held-key-refs.js +5 -0
  38. package/dist/plan/hostnames.d.ts +10 -0
  39. package/dist/plan/hostnames.js +14 -0
  40. package/dist/plan/index.d.ts +5 -0
  41. package/dist/plan/index.js +3 -0
  42. package/dist/plan/live.d.ts +6 -0
  43. package/dist/plan/live.js +3 -2
  44. package/dist/plan/locked-hostnames.d.ts +2 -1
  45. package/dist/plan/locked-hostnames.js +16 -3
  46. package/dist/plan/login-host.d.ts +9 -20
  47. package/dist/plan/login-host.js +10 -25
  48. package/dist/plan/mail-relay.d.ts +48 -29
  49. package/dist/plan/mail-relay.js +145 -68
  50. package/dist/plan/network-move.d.ts +20 -0
  51. package/dist/plan/network-move.js +41 -0
  52. package/dist/plan/oikos-ingress.d.ts +111 -0
  53. package/dist/plan/oikos-ingress.js +231 -0
  54. package/dist/plan/org-names.d.ts +8 -2
  55. package/dist/plan/org-names.js +57 -1
  56. package/dist/plan/own-diff.d.ts +9 -8
  57. package/dist/plan/own-diff.js +42 -34
  58. package/dist/plan/own-hostnames.d.ts +27 -0
  59. package/dist/plan/own-hostnames.js +24 -0
  60. package/dist/plan/own-limits.d.ts +198 -7
  61. package/dist/plan/own-limits.js +163 -32
  62. package/dist/plan/own-services.js +3 -1
  63. package/dist/plan/own-sfu.js +3 -2
  64. package/dist/plan/plan-apply.d.ts +47 -7
  65. package/dist/plan/plan-apply.js +184 -38
  66. package/dist/plan/platform-diff.d.ts +133 -17
  67. package/dist/plan/platform-diff.js +231 -68
  68. package/dist/plan/platform-org.d.ts +79 -10
  69. package/dist/plan/platform-org.js +93 -13
  70. package/dist/plan/platform-providers.d.ts +118 -73
  71. package/dist/plan/platform-providers.js +799 -241
  72. package/dist/plan/proxy-labels.d.ts +57 -11
  73. package/dist/plan/proxy-labels.js +94 -14
  74. package/dist/plan/run-options.d.ts +19 -0
  75. package/dist/plan/run-options.js +46 -0
  76. package/dist/plan/zitadel-login-policy.d.ts +1 -1
  77. package/dist/plan/zitadel-login-policy.js +76 -8
  78. package/dist/platform.d.ts +32 -0
  79. package/dist/platform.js +16 -0
  80. package/dist/provision.d.ts +2 -0
  81. package/dist/provision.js +3 -1
  82. package/dist/slug.d.ts +3 -1
  83. package/dist/slug.js +25 -3
  84. package/dist/store/postgres.js +43 -1
  85. package/dist/store/schema.d.ts +36 -0
  86. package/dist/store/schema.js +4 -0
  87. package/package.json +3 -3
  88. package/versions.digests.json +11 -0
@@ -2,10 +2,12 @@ import { createHash } from 'node:crypto';
2
2
  import { VERSIONS } from '../bootstrap/versions.js';
3
3
  import { APP_ENTRY_KEY } from '../org.js';
4
4
  import { VERSION_CHANGE_KIND, } from '../plan.js';
5
+ import { buildDriftNotes } from './build-record.js';
5
6
  import { versionChangeNeedsConfirmation } from './confirm-upgrade.js';
6
7
  import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
7
8
  import { legacyAppHostname } from './hostnames.js';
8
9
  import { PLATFORM_MAIL_RELAY_KEY, planMailRelay } from './mail-relay.js';
10
+ import { planOikosIngress } from './oikos-ingress.js';
9
11
  import { callsSfuAppliedFingerprint } from './platform-sfu.js';
10
12
  import { ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY, signInDomainBlockedText } from './sign-in-org.js';
11
13
  import { withStepSystem } from './step-system.js';
@@ -219,15 +221,16 @@ export const PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY = 'archon_applied_auth_o
219
221
  */
220
222
  export const PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY = 'archon_applied_rolling_update';
221
223
  /**
222
- * The sign-in org Archon accepts: the platform owner company's own
223
- * ({@link PLATFORM_OWNER_ORG_SCOPE_KEY}), else the platform's `foundry` org for
224
- * a platform with no owner company declared. Undefined when neither is recorded.
224
+ * The owner company's sign-in hostname Archon was last registered under, and
225
+ * the owner's files host (`<internalUrl>|<audience>`, or `none`) its env was
226
+ * last written with. `planArchon` plans the step again when either differs
227
+ * from the owner's current ones (a marker never recorded reads as `none`).
225
228
  */
226
- export function archonAuthOrganizationId(recorded) {
227
- const owner = recorded[PLATFORM_OWNER_ORG_SCOPE_KEY];
228
- if (owner && owner !== 'none')
229
- return owner;
230
- return recorded[PLATFORM_ZITADEL_ORG_SCOPE_KEY] || undefined;
229
+ export const PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY = 'archon_applied_auth_hostname';
230
+ export const PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY = 'archon_applied_files';
231
+ /** The value {@link PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY} records for the owner's files host. */
232
+ export function archonFilesMarker(files) {
233
+ return files ? `${files.internalUrl}|${files.audience}` : 'none';
231
234
  }
232
235
  /**
233
236
  * #175: set to '1' before the step points the tunnel at `http_status:404`
@@ -609,8 +612,78 @@ export function serviceDbEnvAppliedScopeKey(service) {
609
612
  export function zitadelAppIdTokenUserinfoAppliedScopeKey(appKey) {
610
613
  return `zitadel_app_id_token_userinfo_applied.${appKey}`;
611
614
  }
612
- /** The ZITADEL app id of Archon's sign-in client in the platform's own ZITADEL. */
615
+ /**
616
+ * Archon's own sign-in organisation, in the owner company's ZITADEL instance
617
+ * and apart from the organisation that holds the company's people (and Boule's
618
+ * sign-in). Made by the `zitadel:archon-org` step, which runs before
619
+ * `platform:archon`. The first six keys below are the step's own progress:
620
+ * each is recorded only after its call succeeded.
621
+ */
622
+ export const PLATFORM_ARCHON_ORG_SCOPE_KEY = 'zitadel_platform_archon_org';
623
+ /** The primary domain registered on Archon's sign-in organisation. */
624
+ export const PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY = 'zitadel_platform_archon_org_domain';
625
+ /** The domain the sign-in server refused to make primary; set instead of failing, reported as blocked. */
626
+ export const PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY = 'zitadel_platform_archon_org_domain_blocked';
627
+ /** The Archon org id once self-registration is closed on it. */
628
+ export const PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY = 'zitadel_platform_archon_org_registration_closed';
629
+ /** The Archon org id once its login policy forces a second factor. */
630
+ export const PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY = 'zitadel_platform_archon_org_policy';
631
+ /** The first person's user id in the Archon org (`none` when no owner is declared), recorded once the invite was mailed. */
632
+ export const PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY = 'zitadel_platform_archon_owner_invite';
633
+ /** The Archon org the recorded invite was made in. */
634
+ export const PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY = 'zitadel_platform_archon_owner_invite_org';
635
+ /** The name of the Archon sign-in organisation: `<owner slug>-archon`. */
636
+ export function archonOrgName(ownerSlug) {
637
+ return `${ownerSlug}-archon`;
638
+ }
639
+ /** The primary domain of the Archon sign-in organisation: `archon.` before the owner's own sign-in domain. */
640
+ export function archonOrgDomain(ownerSignInDomain) {
641
+ return `archon.${ownerSignInDomain}`;
642
+ }
643
+ /**
644
+ * The CI organisation id the last Archon apply wrote as `CI_ORGANIZATION_ID`,
645
+ * and the `ARCHON_FIRST_OWNER_LINK` value it wrote (empty when none).
646
+ * `planArchon` plans the step again when either differs from what is wanted.
647
+ */
648
+ export const PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY = 'archon_applied_ci_org';
649
+ export const PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY = 'archon_applied_first_owner_link';
650
+ /**
651
+ * `<archonUserId>:<ciUserId>`: the first person in both organisations, which
652
+ * Archon seeds its link table from. Empty when either id is not known (no
653
+ * owner declared, or the CI-side account not recorded yet).
654
+ */
655
+ export function archonFirstOwnerLink(recordedIds, ownerSignIn) {
656
+ const archonUserId = recordedIds[PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY];
657
+ const ciUserId = ownerSignIn?.firstOwnerUserId;
658
+ if (!archonUserId || archonUserId === 'none' || !ciUserId || ciUserId === 'none')
659
+ return '';
660
+ return `${archonUserId}:${ciUserId}`;
661
+ }
662
+ /**
663
+ * The one-time-PIN provider id the Archon Access app was last made with.
664
+ * Drift is read against it, because a plan must not ask edge to find or make
665
+ * the provider.
666
+ */
667
+ export const PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY = 'cloudflare_platform_archon_access_pin';
668
+ /**
669
+ * The Archon org the `AUTH_APP_KEY` in Archon's env was minted for. A key
670
+ * minted for another org (the CI org, before the move) is replaced by a fresh
671
+ * one, so the key the CI-org broker entry held stops being Archon's.
672
+ */
673
+ export const PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY = 'archon_app_key_org';
674
+ /**
675
+ * `<org>|<project>|<app>` of Archon's old sign-in client while it still waits
676
+ * to be deleted. Set before the new client's ids overwrite the recorded ones,
677
+ * cleared once the old client is gone.
678
+ */
679
+ export const PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY = 'zitadel_platform_archon_retired_client';
680
+ /** The ZITADEL app id of Archon's sign-in client in the owner company's ZITADEL. */
613
681
  export const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = 'zitadel_platform_archon_app';
682
+ /** The ZITADEL project id Archon's sign-in client lives in. */
683
+ export const PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY = 'zitadel_platform_archon_project';
684
+ /** The ZITADEL project and client ids of the Access sign-in provider, in the owner company's ZITADEL. */
685
+ export const PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY = 'zitadel_platform_access_project';
686
+ export const PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY = 'zitadel_platform_access_app';
614
687
  /**
615
688
  * The ZITADEL app id of Archon's sign-in client once "user info inside the
616
689
  * ID token" was switched on for it. `planArchonIdTokenUserinfo` plans the
@@ -674,7 +747,7 @@ export function appliedAppDomains(org, recorded, entryKey) {
674
747
  * `coolifyApp` records as applied, so a declared change replans and no secret
675
748
  * is written into it.
676
749
  */
677
- export function resolveAppEnv(org, entry, recorded, available = { platformMailKey: false, sharedSecret: false }) {
750
+ export function resolveAppEnv(org, entry, recorded, available = { sharedSecret: false }) {
678
751
  const sources = {
679
752
  filesOrganisationId: recorded[`files:organisation:${org.id}`],
680
753
  aiOrganisationId: recorded[`ai:organisation:${org.id}`],
@@ -686,15 +759,6 @@ export function resolveAppEnv(org, entry, recorded, available = { platformMailKe
686
759
  const unresolvedSecrets = [];
687
760
  const marker = {};
688
761
  for (const [name, declared] of Object.entries(entry.env).sort(([a], [b]) => (a < b ? -1 : 1))) {
689
- if (typeof declared !== 'string' && declared.from === 'platformMailKey') {
690
- if (available.platformMailKey) {
691
- secrets.push({ name, source: 'platformMailKey' });
692
- marker[name] = 'secret:platformMailKey';
693
- }
694
- else
695
- unresolvedSecrets.push([name, 'platformMailKey']);
696
- continue;
697
- }
698
762
  if (typeof declared !== 'string' && declared.from === 'sharedSecret') {
699
763
  const label = `sharedSecret ${declared.name}`;
700
764
  if (available.sharedSecret) {
@@ -729,6 +793,13 @@ export const PLATFORM_AI_KEY = 'platform:ai';
729
793
  * public hostname. Holds whichever way the public-hostname decision goes.
730
794
  */
731
795
  export const PLATFORM_AI_INTERNAL_URL_SCOPE_KEY = 'ai_platform_internal_url';
796
+ /**
797
+ * The id of the tunnel that carries the AI host's public hostname. Recorded once the tunnel, its
798
+ * connector and the DNS record are in place; an entry is given `AI_URL` only after it.
799
+ */
800
+ export const PLATFORM_AI_TUNNEL_SCOPE_KEY = 'cloudflare_platform_ai_tunnel';
801
+ /** The one Coolify app that runs `cloudflared` for the AI tunnel. */
802
+ export const PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY = 'coolify_platform_ai_connector_app';
732
803
  /**
733
804
  * #172: the same two-key discipline as the ZITADEL admin key above, for
734
805
  * foundry's AI management key (the bearer key `ai:organisation`, `ai:budget`
@@ -935,6 +1006,18 @@ export const PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY = 'hc_applied_git_ref';
935
1006
  * and that implementation is where the key is actually used.
936
1007
  */
937
1008
  export const PLATFORM_HC_API_KEY_REF = 'hc:platform:apiKey';
1009
+ /**
1010
+ * foundry#544: the held-key ref a human puts the *outside* Healthchecks
1011
+ * account's project API key under, read only while
1012
+ * `platform.heartbeatsProvider` is `'hosted'` -- {@link PLATFORM_HC_API_KEY_REF}'s
1013
+ * own ref is for the self-hosted instance only, and the two are never
1014
+ * conflated: switching provider needs its own key held under its own ref,
1015
+ * never the other one silently reused. Same "this package never reads the
1016
+ * value back" rule as {@link PLATFORM_HC_API_KEY_REF}'s own doc comment --
1017
+ * whoever implements {@link HeartbeatsPort} against the outside service is
1018
+ * the host.
1019
+ */
1020
+ export const PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF = 'heartbeats:platform:hostedApiKey';
938
1021
  /**
939
1022
  * #540: the heartbeat check named after the uptime Worker (foundry#542,
940
1023
  * which is what actually pings it -- blocked today on edge#26, so nothing
@@ -1134,7 +1217,7 @@ export function uptimeRecorded(recordedIds) {
1134
1217
  * unchanged).
1135
1218
  */
1136
1219
  export function planPlatform(platform, live, options = {}) {
1137
- const { jobsObservable = true, sfuBlocked = null, callsOrgSlugs, mailRoutes = [], mailPlatformDomains = [], mailRoutesBlocked = [], recordedIds = {}, blocked = {}, drifted = {}, } = options;
1220
+ const { jobsObservable = true, sfuBlocked = null, callsOrgSlugs, mailRoutes = [], mailRoutesBlocked = [], recordedIds = {}, owner, liveBuilds = {}, blocked = {}, drifted = {}, } = options;
1138
1221
  const byKey = new Map(live.map((r) => [resourceKey(r.system, r.kind, r.key), r]));
1139
1222
  const claimed = new Set();
1140
1223
  const plannedSteps = [];
@@ -1169,6 +1252,15 @@ export function planPlatform(platform, live, options = {}) {
1169
1252
  : (service) => `the platform's ${service} is blocked, not planned: every DNS record is written through edge, and edge is neither applied nor planned (${platform.edge.enabled
1170
1253
  ? (edge.blocked ?? 'its step is not planned')
1171
1254
  : 'the platform does not enable edge'})`;
1255
+ // The shared oikos.host ingress: after edge, whose DNS and tunnel calls it uses.
1256
+ const oikosIngress = planOikosIngress(platform, recordedIds, {
1257
+ // Only once edge is APPLIED: the zone check in `blockedSteps` needs edge's address, so a
1258
+ // same-round plan would apply unchecked and throw on a missing zone.
1259
+ edgeApplied,
1260
+ executorBlocked: blocked.oikosIngress,
1261
+ });
1262
+ if (oikosIngress.step)
1263
+ plannedSteps.push(oikosIngress.step);
1172
1264
  // #540: hc needs only edge's DNS, the same as mail and archon -- no
1173
1265
  // auth/ZITADEL client, so it is planned here rather than after `authReady`
1174
1266
  // further down.
@@ -1203,27 +1295,27 @@ export function planPlatform(platform, live, options = {}) {
1203
1295
  // mail step that gives the port up.
1204
1296
  const relay = planMailRelay(platform, recordedIds, {
1205
1297
  routes: mailRoutes,
1206
- platformDomains: mailPlatformDomains,
1207
1298
  executorBlocked: blocked.relay,
1208
- mailReady: mail.step !== null ||
1209
- (Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_MAIL_KEY)]) &&
1210
- mailPortsApplied(platform, recordedIds)),
1211
1299
  });
1212
1300
  if (relay.step)
1213
1301
  plannedSteps.push(relay.step);
1214
1302
  // #172: a throw halts every step after it, so this one goes late.
1215
- const ai = planAi(platform, recordedIds, blocked.ai);
1303
+ const ai = planAi(platform, recordedIds, blocked.ai ?? dnsBlocked?.('AI host'));
1216
1304
  if (ai.step)
1217
1305
  plannedSteps.push(ai.step);
1218
1306
  // #604: after edge and sign-in, whose applied ids it reads; before Archon,
1219
1307
  // whose redeploy can end the run.
1220
- const accessProvider = planAccessProvider(platform, recordedIds, blocked.accessProvider);
1308
+ const accessProvider = planAccessProvider(platform, recordedIds, blocked.accessProvider, owner?.signIn ?? null);
1221
1309
  if (accessProvider.step)
1222
1310
  plannedSteps.push(accessProvider.step);
1311
+ // Archon's own sign-in organisation: before Archon, whose client, env and broker entry live in it.
1312
+ const archonOrg = planArchonOrg(platform, recordedIds, blocked.archonOrg, blocked.archonOrgInvite, owner?.signIn ?? null);
1313
+ if (archonOrg.step)
1314
+ plannedSteps.push(archonOrg.step);
1223
1315
  // #175: last, on purpose. Archon's sign-in client lives in the sign-in
1224
1316
  // `planAuth` builds, and its env reads what every other service's step
1225
1317
  // records or delivers, so in one apply it runs after all of them.
1226
- const archon = planArchon(platform, recordedIds, blocked.archon ?? dnsBlocked?.('Archon'), authReady, drifted.archon, blocked.archonDeployRunning);
1318
+ const archon = planArchon(platform, recordedIds, blocked.archon ?? dnsBlocked?.('Archon'), owner?.signIn ?? null, owner?.files ?? null, drifted.archon, blocked.archonDeployRunning, { step: archonOrg.step !== null, blocked: archonOrg.blocked });
1227
1319
  // The host runs inside this app: its step ends by redeploying the process
1228
1320
  // that runs the plan, so it goes after everything else (pushed below).
1229
1321
  if (archon.step && !blocked.archonRunsInsideApp)
@@ -1328,11 +1420,17 @@ export function planPlatform(platform, live, options = {}) {
1328
1420
  unmanaged.push({ description: ai.blocked, key: 'ai' });
1329
1421
  if (edge.blocked)
1330
1422
  unmanaged.push({ description: edge.blocked, key: 'edge' });
1423
+ // Edge's own blocked reason, just above, already names the cause when edge is what is missing.
1424
+ if (oikosIngress.blocked && !edge.blocked) {
1425
+ unmanaged.push({ description: oikosIngress.blocked, key: 'oikos-ingress' });
1426
+ }
1331
1427
  if (accessProvider.blocked) {
1332
1428
  unmanaged.push({ description: accessProvider.blocked, key: 'access-provider' });
1333
1429
  }
1334
1430
  if (hc.blocked)
1335
1431
  unmanaged.push({ description: hc.blocked, key: 'hc' });
1432
+ if (archonOrg.blocked)
1433
+ unmanaged.push({ description: archonOrg.blocked, key: 'archon-org' });
1336
1434
  if (archon.blocked)
1337
1435
  unmanaged.push({ description: archon.blocked, key: 'archon' });
1338
1436
  // #398: the old integrations key stays pinned until Archon holds the new
@@ -1355,7 +1453,7 @@ export function planPlatform(platform, live, options = {}) {
1355
1453
  plannedSteps: plannedSteps.map(withStepSystem),
1356
1454
  unmanaged,
1357
1455
  versionChanges,
1358
- notes: uptime.notes,
1456
+ notes: [...uptime.notes, ...buildDriftNotes(recordedIds, liveBuilds)],
1359
1457
  };
1360
1458
  }
1361
1459
  /**
@@ -1492,19 +1590,13 @@ function planJobs(platform, byKey, claimed, observable, recordedIds) {
1492
1590
  * never a wrong step.
1493
1591
  */
1494
1592
  export const PLATFORM_BACKUP_DATABASE_SCOPE_KEYS = {
1495
- jobs: 'coolify_jobs_database',
1496
- auth: 'coolify_platform_auth_database',
1497
1593
  archon: 'coolify_platform_archon_database',
1498
1594
  ai: 'coolify_platform_ai_database',
1499
- files: 'coolify_platform_files_database',
1500
1595
  integrations: 'coolify_integrations_database',
1501
1596
  };
1502
1597
  export const PLATFORM_BACKUP_SERVICE_KEYS = [
1503
- 'jobs',
1504
- 'auth',
1505
1598
  'archon',
1506
1599
  'ai',
1507
- 'files',
1508
1600
  'integrations',
1509
1601
  ];
1510
1602
  /**
@@ -2383,16 +2475,16 @@ function planAuthSignInPolicy(platform, recordedIds, wired) {
2383
2475
  * delivered (its secret cannot be shown again), or `executorBlocked` (no
2384
2476
  * dependencies, or edge cannot confirm Access is ready).
2385
2477
  */
2386
- function planAccessProvider(platform, recordedIds, executorBlocked) {
2387
- if (!platform.edge.enabled || !platform.auth.enabled)
2478
+ function planAccessProvider(platform, recordedIds, executorBlocked, ownerSignIn) {
2479
+ if (!platform.edge.enabled)
2388
2480
  return { step: null, blocked: null };
2389
2481
  if (recordedIds[ACCESS_PROVIDER_RECORD_KEY]) {
2390
2482
  // The secret is not stored, so a changed address cannot be re-sent: a person remakes the client.
2391
2483
  const appliedHostname = recordedIds[PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY];
2392
- if (appliedHostname && appliedHostname !== platform.auth.hostname) {
2484
+ if (ownerSignIn && appliedHostname && appliedHostname !== ownerSignIn.hostname) {
2393
2485
  return {
2394
2486
  step: null,
2395
- blocked: `the Access sign-in provider is blocked, not planned: it was applied with the sign-in hostname ${appliedHostname} and ${platform.auth.hostname} is declared now; its secret is not stored, so delete the ZITADEL client cloudflare-access, remove the recorded keys ${ACCESS_PROVIDER_RECORD_KEY}, ${PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY} and ${PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY}, then apply again`,
2487
+ blocked: `the Access sign-in provider is blocked, not planned: it was applied with the sign-in hostname ${appliedHostname} and ${ownerSignIn.hostname} is the owner company's now; its secret is not stored, so delete the ZITADEL client cloudflare-access, remove the recorded keys ${ACCESS_PROVIDER_RECORD_KEY}, ${PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY} and ${PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY}, then apply again`,
2396
2488
  };
2397
2489
  }
2398
2490
  return { step: null, blocked: null };
@@ -2404,15 +2496,10 @@ function planAccessProvider(platform, recordedIds, executorBlocked) {
2404
2496
  blocked: `the Access sign-in provider is blocked, not planned: its ZITADEL client (${undelivered}) was made and its secret may or may not have reached edge; it cannot be shown again, so delete that client in ZITADEL and remove the recorded keys ${PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY} and ${ACCESS_PROVIDER_RECORD_KEY}, then apply again to make both anew`,
2405
2497
  };
2406
2498
  }
2407
- const applied = Boolean(recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]) &&
2408
- Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_AUTH_KEY)]) &&
2409
- // The sibling ZITADEL steps need the delivered admin key (or the instance it came from).
2410
- (Boolean(recordedIds[PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY]) ||
2411
- Boolean(recordedIds[PLATFORM_AUTH_INSTANCE_SCOPE_KEY]));
2412
- if (!applied) {
2499
+ if (!ownerSignIn || !recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]) {
2413
2500
  return {
2414
2501
  step: null,
2415
- blocked: 'the Access sign-in provider is blocked, not planned: edge and the platform sign-in are not both applied yet',
2502
+ blocked: "the Access sign-in provider is blocked, not planned: edge and the owner company's sign-in are not both applied yet",
2416
2503
  };
2417
2504
  }
2418
2505
  if (executorBlocked)
@@ -2421,7 +2508,7 @@ function planAccessProvider(platform, recordedIds, executorBlocked) {
2421
2508
  step: {
2422
2509
  action: { system: 'cloudflare', kind: 'access-provider', key: 'platform', desired: {} },
2423
2510
  step: {
2424
- description: "make the platform's ZITADEL the sign-in provider in Cloudflare Access, for company gates",
2511
+ description: "make the owner company's ZITADEL the sign-in provider in Cloudflare Access, for company gates",
2425
2512
  destroys: false,
2426
2513
  migrates: false,
2427
2514
  },
@@ -2511,9 +2598,24 @@ function planHc(platform, recordedIds, executorBlocked) {
2511
2598
  * with. Without this, a fresh platform's heartbeats step is ready
2512
2599
  * (`hcReady`) the same round `hc` is merely *planned*, long before anyone
2513
2600
  * has signed in to it to create that key.
2601
+ *
2602
+ * foundry#544: `platform.heartbeatsProvider === 'hosted'` skips the two
2603
+ * checks above that are specific to the self-hosted server (`platform.hc.enabled`,
2604
+ * `hcReady`) -- the outside service needs neither `hc` declared nor applied.
2605
+ * `apiKeyMissing` still applies in both cases, but names a different ref
2606
+ * depending on the provider (the caller, `platform-providers.ts`'s
2607
+ * `blockedSteps`, resolves the right one before calling this). `desired`
2608
+ * folds the provider in alongside the check names, so switching provider
2609
+ * with the same checks otherwise desired is still a change from the last
2610
+ * recorded marker -- the step plans again, creating every check fresh on
2611
+ * the newly-chosen provider (`provisionPlatformHeartbeats` lists the new
2612
+ * provider's own live checks, sees none of foundry's names yet, and creates
2613
+ * all of them) rather than reading "nothing left to reconcile" against a
2614
+ * marker that was true of the *other* provider.
2514
2615
  */
2515
2616
  function planHeartbeats(platform, recordedIds, hcReady, executorMissing, apiKeyMissing) {
2516
- if (!platform.hc.enabled) {
2617
+ const hosted = platform.heartbeatsProvider === 'hosted';
2618
+ if (!hosted && !platform.hc.enabled) {
2517
2619
  // Removing checks is not planned. The recorded marker names the checks
2518
2620
  // foundry's last reconcile wanted, so they are foundry's own; report
2519
2621
  // them rather than leave them unseen.
@@ -2525,7 +2627,7 @@ function planHeartbeats(platform, recordedIds, hcReady, executorMissing, apiKeyM
2525
2627
  : null,
2526
2628
  };
2527
2629
  }
2528
- if (!hcReady) {
2630
+ if (!hosted && !hcReady) {
2529
2631
  return {
2530
2632
  step: null,
2531
2633
  blocked: 'the heartbeat checks are blocked, not planned: platform:hc has not been applied yet',
@@ -2540,10 +2642,12 @@ function planHeartbeats(platform, recordedIds, hcReady, executorMissing, apiKeyM
2540
2642
  if (apiKeyMissing) {
2541
2643
  return {
2542
2644
  step: null,
2543
- blocked: `the heartbeat checks are blocked, not planned: no project API key is held yet for hc (${PLATFORM_HC_API_KEY_REF}); a human must sign in to hc, create the first superuser account and a project, mint that project's API key, and hold it under that ref (issue #540 "Human steps") before this step can call hc's check-creation API`,
2645
+ blocked: hosted
2646
+ ? `the heartbeat checks are blocked, not planned: no project API key is held yet for the outside Healthchecks account (${PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF}); a human must create that account (or project), mint its API key, and hold it under that ref (issue #544 "Human steps") before this step can call its check-creation API`
2647
+ : `the heartbeat checks are blocked, not planned: no project API key is held yet for hc (${PLATFORM_HC_API_KEY_REF}); a human must sign in to hc, create the first superuser account and a project, mint that project's API key, and hold it under that ref (issue #540 "Human steps") before this step can call hc's check-creation API`,
2544
2648
  };
2545
2649
  }
2546
- const desired = [...desiredHeartbeatCheckNames(recordedIds)].sort().join(',');
2650
+ const desired = `${platform.heartbeatsProvider}:${[...desiredHeartbeatCheckNames(recordedIds)].sort().join(',')}`;
2547
2651
  if (recordedIds[PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY] === desired) {
2548
2652
  return { step: null, blocked: null };
2549
2653
  }
@@ -2698,7 +2802,9 @@ function planAi(platform, recordedIds, executorBlocked) {
2698
2802
  // already set then, so nothing is minted again.
2699
2803
  const applied = Boolean(recordedIds[key]);
2700
2804
  const drifted = recordedIds[PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY] !== platform.ai.hostname ||
2701
- recordedIds[PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY] !== platform.ai.gitRef;
2805
+ recordedIds[PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY] !== platform.ai.gitRef ||
2806
+ // The public route (tunnel, connector, DNS record) is part of the step: a host applied before it existed plans it once.
2807
+ !recordedIds[PLATFORM_AI_TUNNEL_SCOPE_KEY];
2702
2808
  if (applied && !drifted)
2703
2809
  return { step: null, blocked: null };
2704
2810
  // #201: the signing pair foundry mints to authenticate its own calls to
@@ -2788,13 +2894,67 @@ function planAi(platform, recordedIds, executorBlocked) {
2788
2894
  * ({@link PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY}) plans the step again, as
2789
2895
  * {@link planAuth} does.
2790
2896
  */
2791
- function planArchon(platform, recordedIds, executorBlocked, authReady, drifted, deployRunning) {
2897
+ /** Archon signs its operators in through the owner company's own sign-in; fixed text, part of the stale-plan comparison. */
2898
+ export const ARCHON_OWNER_SIGN_IN_BLOCKED = "Archon is blocked, not planned: the owner company's sign-in is not applied yet";
2899
+ /** Archon signs in from its own organisation, which the `zitadel:archon-org` step makes; fixed text, part of the stale-plan comparison. */
2900
+ export const ARCHON_ORG_BLOCKED = 'Archon is blocked, not planned: its own sign-in organisation is not applied yet';
2901
+ /**
2902
+ * Archon's own sign-in organisation in the owner company's ZITADEL: the org,
2903
+ * its primary domain, closed registration, the second-factor policy, and the
2904
+ * first person's invite. One `zitadel:archon-org` step keyed
2905
+ * {@link PLATFORM_ARCHON_KEY}, planned while any of those is not recorded
2906
+ * against the recorded org, and always before `platform:archon`. Blocked, not
2907
+ * planned, while the host cannot reach the instance. A refused domain is
2908
+ * reported as a standing note (as the owner org's is) and does not hold the
2909
+ * step; an invite that cannot be sent (no work address, no mail port) is a
2910
+ * standing note and does not hold it either.
2911
+ */
2912
+ function planArchonOrg(platform, recordedIds, executorBlocked, inviteBlocked, ownerSignIn) {
2913
+ if (!platform.archon.enabled || !ownerSignIn)
2914
+ return { step: null, blocked: null };
2915
+ const orgId = recordedIds[PLATFORM_ARCHON_ORG_SCOPE_KEY];
2916
+ const blockedDomain = recordedIds[PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY];
2917
+ const domainDone = Boolean(recordedIds[PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY]) || Boolean(blockedDomain);
2918
+ const inviteRecorded = recordedIds[PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY];
2919
+ const inviteDone = Boolean(inviteBlocked) ||
2920
+ inviteRecorded === 'none' ||
2921
+ (Boolean(inviteRecorded) && recordedIds[PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY] === orgId);
2922
+ const standing = [
2923
+ inviteBlocked,
2924
+ blockedDomain && !recordedIds[PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY]
2925
+ ? `Archon's sign-in domain ${blockedDomain} is blocked, not planned: the sign-in server refused to make it primary because its domain policy wants proof of ownership. Set the policy ValidateOrgDomains to false on the sign-in server, then remove the recorded key ${PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY}`
2926
+ : null,
2927
+ ].filter((v) => Boolean(v));
2928
+ const standingNote = standing.length > 0 ? standing.join('; ') : null;
2929
+ if (orgId &&
2930
+ domainDone &&
2931
+ recordedIds[PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY] === orgId &&
2932
+ recordedIds[PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY] === orgId &&
2933
+ inviteDone) {
2934
+ return { step: null, blocked: standingNote };
2935
+ }
2936
+ if (executorBlocked)
2937
+ return { step: null, blocked: executorBlocked };
2938
+ return {
2939
+ step: {
2940
+ action: { system: 'zitadel', kind: 'archon-org', key: PLATFORM_ARCHON_KEY, desired: {} },
2941
+ step: {
2942
+ description: "make Archon's own sign-in organisation in the owner company's ZITADEL: close registration, force a second factor, and invite the first person",
2943
+ destroys: false,
2944
+ migrates: false,
2945
+ },
2946
+ },
2947
+ blocked: standingNote,
2948
+ };
2949
+ }
2950
+ function planArchon(platform, recordedIds, executorBlocked, ownerSignIn, ownerFiles, drifted, deployRunning, archonOrg = { step: false, blocked: null }) {
2792
2951
  if (!platform.archon.enabled)
2793
2952
  return { step: null, blocked: null };
2794
2953
  // A deploy the step triggered is still running: planning it again would deploy twice.
2795
2954
  if (deployRunning)
2796
2955
  return { step: null, blocked: deployRunning };
2797
2956
  const key = resourceKey('coolify', 'app', PLATFORM_ARCHON_KEY);
2957
+ const archonOrgId = recordedIds[PLATFORM_ARCHON_ORG_SCOPE_KEY];
2798
2958
  const applied = Boolean(recordedIds[key]) &&
2799
2959
  Boolean(recordedIds[PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY]) &&
2800
2960
  recordedIds[PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY] === platform.archon.hostname &&
@@ -2803,11 +2963,20 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted,
2803
2963
  // deleted by hand, or `platform:auth` minting a fresh one) leaves
2804
2964
  // Archon's last registration on a broker that no longer exists, and
2805
2965
  // nothing else here would ever plan the step again to redo it.
2806
- Boolean(recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY]) &&
2807
- recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ===
2808
- recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY] &&
2809
- recordedIds[PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY] ===
2810
- archonAuthOrganizationId(recordedIds) &&
2966
+ ownerSignIn !== null &&
2967
+ recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] === ownerSignIn.brokerAppId &&
2968
+ // Archon signs in from its own organisation, never the company's.
2969
+ Boolean(archonOrgId) &&
2970
+ recordedIds[PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY] === archonOrgId &&
2971
+ recordedIds[PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY] === ownerSignIn.organisationId &&
2972
+ (recordedIds[PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY] ?? '') ===
2973
+ archonFirstOwnerLink(recordedIds, ownerSignIn) &&
2974
+ recordedIds[PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY] === archonOrgId &&
2975
+ !recordedIds[PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY] &&
2976
+ Boolean(recordedIds[PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY]) &&
2977
+ recordedIds[PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY] === ownerSignIn.hostname &&
2978
+ (recordedIds[PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY] ?? 'none') ===
2979
+ archonFilesMarker(ownerFiles) &&
2811
2980
  // #475: a platform applied before the rolling-update fix keeps planning
2812
2981
  // this step until a confirmed redeploy has cleared it. #480: this marker
2813
2982
  // alone does not catch the live app's settings changing back afterward
@@ -2839,17 +3008,11 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted,
2839
3008
  recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY]);
2840
3009
  if (applied && !drifted)
2841
3010
  return { step: null, blocked: null };
2842
- if (!platform.auth.enabled) {
2843
- return {
2844
- step: null,
2845
- blocked: "Archon is blocked, not planned: its sign-in client lives in the platform's own sign-in, and auth is not enabled",
2846
- };
2847
- }
2848
- if (!authReady) {
2849
- return {
2850
- step: null,
2851
- blocked: "Archon is blocked, not planned: its sign-in client lives in the platform's own sign-in, which is neither applied nor planned",
2852
- };
3011
+ if (!ownerSignIn)
3012
+ return { step: null, blocked: ARCHON_OWNER_SIGN_IN_BLOCKED };
3013
+ // The org is recorded, or its step runs earlier in this same plan.
3014
+ if (!archonOrgId && !archonOrg.step) {
3015
+ return { step: null, blocked: archonOrg.blocked ?? ARCHON_ORG_BLOCKED };
2853
3016
  }
2854
3017
  if (executorBlocked)
2855
3018
  return { step: null, blocked: executorBlocked };
@@ -1,6 +1,6 @@
1
- import { type DeclaredOrg } from '../org.js';
1
+ import { type DeclaredOrg, type VersionPin } from '../org.js';
2
2
  import type { DeclaredPlatform } from '../platform.js';
3
- import type { AgoraHostValues } from './agora.js';
3
+ import { type AgoraHostValues } from './agora.js';
4
4
  /**
5
5
  * #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
6
6
  * fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
@@ -76,8 +76,8 @@ export interface OrgPlatformReadiness {
76
76
  /** The AI host (#172), the same always-on shape as {@link filesReady}. */
77
77
  aiReady: boolean;
78
78
  /**
79
- * The AI host's internal address, which an entry with `uses.ai` gets as
80
- * `AI_URL`. `null` or absent until the platform has recorded it.
79
+ * The AI host's public origin (`https://<platform.ai.hostname>`), which an entry with `uses.ai`
80
+ * gets as `AI_URL`. `null` or absent until the platform has applied the AI host.
81
81
  */
82
82
  aiUrl?: string | null;
83
83
  /**
@@ -87,8 +87,30 @@ export interface OrgPlatformReadiness {
87
87
  * read, so "applied" has no separate check.
88
88
  */
89
89
  callsReady: boolean;
90
+ /** The shared oikos.host ingress (tunnel, wildcard record, rate limit) is applied. Absent reads as false. */
91
+ oikosIngressApplied?: boolean;
92
+ /**
93
+ * The broker app Archon last registered on (`PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY`). The
94
+ * owner's own sign-in counts that registration as its broker's first entry: the broker starts
95
+ * on it and the login client is minted once it is there. Absent or `null` when Archon has not
96
+ * registered.
97
+ */
98
+ archonBrokerAppId?: string | null;
99
+ /**
100
+ * The ZITADEL project and client ids the platform made in the owner company's own ZITADEL
101
+ * (Archon's and the Access provider's). The owner company's page counts them as the
102
+ * platform's, not as strangers. Absent when none is recorded.
103
+ */
104
+ platformZitadelIds?: readonly string[];
90
105
  /** The platform's own sign-in (#168), which every org's own ZITADEL org lives inside. */
91
106
  authReady: boolean;
107
+ /**
108
+ * The host of the platform's own shared sign-in when its Coolify app is still recorded
109
+ * (`coolify:app:platform:auth`), whether or not this build declares it. Absent on a platform
110
+ * built from empty. The owner company's own sign-in takes the same name on the apex, so it
111
+ * waits while this is set.
112
+ */
113
+ platformAuthRecordedHost?: string;
92
114
  /** `https://<platform.auth.hostname>`, the platform's own sign-in issuer; `null` until {@link authReady}. The owner invite reads its issuer here, never from Agora's host config. */
93
115
  authIssuer: string | null;
94
116
  /** The Access provider id the platform recorded (`ACCESS_PROVIDER_RECORD_KEY`), which every gate requires. `null` or absent until the platform has recorded it. */
@@ -142,7 +164,7 @@ export interface OrgPlatformReadiness {
142
164
  };
143
165
  /**
144
166
  * The platform's Lettermint key (`PLATFORM_AUTH_BROKER_MAIL_KEY_REF`) is held,
145
- * so an entry env from `platformMailKey` can be filled. Absent reads as not held.
167
+ * so the owner company's mail can be planned. Absent reads as not held.
146
168
  */
147
169
  mailKeyHeld?: boolean;
148
170
  /** The host's held keys can `put`, so an entry env from `sharedSecret` can be filled. Absent reads as false. */
@@ -160,9 +182,9 @@ export interface OrgPlatformReadiness {
160
182
  jobs: string;
161
183
  };
162
184
  /**
163
- * The platform's recorded owner invite (`PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY`): the user id the
164
- * platform's files host takes as `AUTHZ_BOOTSTRAP_OPERATOR`, or `'none'` when the platform has no
165
- * owner to invite. `null` or absent until recorded. An own org's files host gets the same value.
185
+ * The user id an own org's files host takes as `AUTHZ_BOOTSTRAP_OPERATOR`: the owner company's
186
+ * owner in the owner company's own ZITADEL ({@link platformOwnerUserIdFrom}), or `'none'` when
187
+ * the owner company has no owner to invite. `null` or absent until recorded.
166
188
  */
167
189
  platformOwnerUserId?: string | null;
168
190
  }
@@ -221,8 +243,8 @@ heldRefs?: ReadonlySet<string>,
221
243
  noKeyReader?: boolean,
222
244
  /** #317: true when the host's executor was built with an `EdgePort` ({@link OrgPlatformReadiness.edgeReady}). */
223
245
  edgePortPresent?: boolean): OrgPlatformReadiness;
224
- /** The git refs the platform declares for the services an own org deploys. */
225
- export declare function serviceGitRefsOf(platform: DeclaredPlatform): {
246
+ /** The git refs the platform declares for the services an own org deploys; a company's `pin` wins per part. */
247
+ export declare function serviceGitRefsOf(platform: DeclaredPlatform, pin?: VersionPin | null): {
226
248
  auth: string;
227
249
  mail: string;
228
250
  files: string;
@@ -257,3 +279,50 @@ export declare function agoraHostValuesForOwn(org: DeclaredOrg, orgRecordedIds:
257
279
  * host-supplied `agora` port overrides it.
258
280
  */
259
281
  export declare function agoraHostValuesFromPlatform(platform: DeclaredPlatform, platformRecordedIds: Readonly<Record<string, string>>): AgoraHostValues;
282
+ /**
283
+ * The owner company's own sign-in, as the platform plan reads it. `null` (the
284
+ * platform plan's input) until the owner's own sign-in step and its ZITADEL
285
+ * organisation are applied. `bearerRef` is a held-key reference, never a value.
286
+ */
287
+ export interface OwnerSignIn {
288
+ issuerUrl: string;
289
+ organisationId: string;
290
+ brokerAppId: string;
291
+ bearerRef: string;
292
+ hostname: string;
293
+ /**
294
+ * The company's first owner in its own organisation (the CI-side half of
295
+ * Archon's first-owner link): the recorded user id, `'none'` when no owner
296
+ * is declared, absent while the owner step has recorded nothing.
297
+ */
298
+ firstOwnerUserId?: string | null;
299
+ }
300
+ /** The owner company's own files host, as Archon's `FILES_*` env reads it. */
301
+ export interface OwnerFiles {
302
+ internalUrl: string;
303
+ audience: string;
304
+ organisationId: string;
305
+ managementKeyRef: string;
306
+ }
307
+ /** What the platform plan reads about the owner company. */
308
+ export interface PlatformOwnerInput {
309
+ signIn?: OwnerSignIn | null;
310
+ files?: OwnerFiles | null;
311
+ }
312
+ /**
313
+ * The owner's sign-in from its declared org and the ids its own steps recorded.
314
+ * The host and issuer are the host the own-auth step last applied, and only
315
+ * while that is the declared `ownHostnames(org).auth`: a domain edit gives
316
+ * `null` until the step has applied the new host, so the owner's instance
317
+ * token is never sent to a host the step has not set up.
318
+ */
319
+ export declare function ownerSignInFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): OwnerSignIn | null;
320
+ /**
321
+ * The platform's operator identity for a company's files host: the owner company's first owner,
322
+ * as the owner's `zitadel:owner` step recorded it in the owner company's own ZITADEL. `'none'`
323
+ * when the owner company declares no owner; `null` until the step has recorded the id, and
324
+ * when there is no owner company running its own services.
325
+ */
326
+ export declare function platformOwnerUserIdFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): string | null;
327
+ /** The owner's files host once its own files step has applied; `null` before. */
328
+ export declare function ownerFilesFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): OwnerFiles | null;