@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
@@ -7,20 +7,24 @@ import { AlreadyRunningError, StepFailedError } from '../runner/errors.js';
7
7
  import { StaleLeaseError, } from '../store.js';
8
8
  import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
9
9
  import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
10
+ import { NetworkMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
11
+ import { readLiveBuilds } from './build-record.js';
10
12
  import { unconfirmedUpgrades } from './confirm-upgrade.js';
11
- import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, isPlatformOwnOrg, toPlan, } from './diff.js';
13
+ import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
12
14
  import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
13
15
  import { BOULE_EDGE_ENTRY_KEY, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
14
16
  import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
15
17
  import { readLivePlatformResources, readLiveResources, } from './live.js';
16
18
  import { lockedHostnames } from './locked-hostnames.js';
17
19
  import { agoraIssuerHostFor, loginHostServesAgora, readLoginHostDiscovery, } from './login-host.js';
18
- import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, } from './mail-relay.js';
20
+ import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMTP_PORT_MAX, OWN_MAIL_SMTP_PORT_SCOPE_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, isOwnMailSmtpPort, mailRelayFingerprint, } from './mail-relay.js';
19
21
  import { findNameClashes, nameClashMessage } from './org-names.js';
22
+ import { ownHostnames } from './own-hostnames.js';
20
23
  import { OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, orgTarget } from './own-services.js';
21
- import { PLATFORM_ARCHON_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
22
- import { DEFAULT_ORG_PLATFORM_READINESS, NO_DOMAIN_MAIL_APEX, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, dnsBlockedReason, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, serviceGitRefsOf, } from './platform-org.js';
23
- import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
24
+ import { PLATFORM_ARCHON_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
25
+ import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, dnsBlockedReason, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, ownerFilesFrom, ownerSignInFrom, platformOwnerUserIdFrom, serviceGitRefsOf, } from './platform-org.js';
26
+ import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY, } from './platform-providers.js';
27
+ import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, } from './proxy-labels.js';
24
28
  import { liveWorkers, workerTraceScopeKeys } from './worker.js';
25
29
  /** #441: the lease scope that serializes every mutation of one company, and the one for the shared platform. */
26
30
  const companyScope = (orgId) => `company:${orgId}`;
@@ -160,6 +164,7 @@ export class FoundryPlanApply {
160
164
  now;
161
165
  leaseTtlMs;
162
166
  appliedVersions;
167
+ backupGate;
163
168
  platformDeps;
164
169
  runner;
165
170
  constructor(options) {
@@ -187,6 +192,7 @@ export class FoundryPlanApply {
187
192
  this.now = options.now ?? (() => new Date());
188
193
  this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
189
194
  this.appliedVersions = options.appliedVersions;
195
+ this.backupGate = options.backupGate;
190
196
  this.platformDeps = options.platform;
191
197
  this.runner = options.runner;
192
198
  }
@@ -360,6 +366,7 @@ export class FoundryPlanApply {
360
366
  const reader = this.readers.coolify;
361
367
  const live = project && reader.listProject ? await reader.listProject(project) : null;
362
368
  const claimedElsewhere = [];
369
+ const platformRecorded = new Set(Object.values((await this.platformDeps?.store.getServiceIds()) ?? {}));
363
370
  for (const [what, uuid] of [
364
371
  ['application', input.applicationUuid],
365
372
  ['database', input.databaseUuid],
@@ -367,6 +374,10 @@ export class FoundryPlanApply {
367
374
  ]) {
368
375
  if (!uuid)
369
376
  continue;
377
+ if (platformRecorded.has(uuid)) {
378
+ claimedElsewhere.push(`${what} ${uuid} is recorded by the platform`);
379
+ continue;
380
+ }
370
381
  const owner = await this.store.findOtherOrgRecordingCoolifyId(uuid, id);
371
382
  if (owner)
372
383
  claimedElsewhere.push(`${what} ${uuid} is already recorded by org ${owner}`);
@@ -394,12 +405,50 @@ export class FoundryPlanApply {
394
405
  await this.refuse(plan, `version change without confirmUpgrade naming: ${unconfirmed.map((u) => u.service).join(', ')}`);
395
406
  throw new UpgradeConfirmationRequiredError(unconfirmed);
396
407
  }
408
+ // F6: before step one. No override.
409
+ let backupId = null;
410
+ if (this.backupGate) {
411
+ try {
412
+ ({ backupId } = await requireFreshBackupsForUpgrades(freshPlan, options.confirmUpgrade, {
413
+ coolify: this.backupGate.coolify,
414
+ orgId,
415
+ recordedIds: (await this.store.getAppliedState(orgId))?.serviceIds ?? {},
416
+ now: () => this.now().getTime(),
417
+ }));
418
+ }
419
+ catch (e) {
420
+ if (e instanceof UpgradeBackupMissingError) {
421
+ await this.refuse(plan, `upgrade of ${e.service} without a fresh backup`);
422
+ }
423
+ throw e;
424
+ }
425
+ }
426
+ // A move to the company's own network deletes its databases: a fresh dump of each first.
427
+ if (detail.plannedSteps.some((s) => s.action.system === 'coolify' && s.action.kind === 'network')) {
428
+ try {
429
+ if (!this.backupGate)
430
+ throw new NetworkMoveBackupMissingError('no-gate');
431
+ await requireFreshBackupsForMove({
432
+ coolify: this.backupGate.coolify,
433
+ projectUuid: (await this.store.getAppliedState(orgId))?.serviceIds
434
+ ?.coolify_project,
435
+ ownsMail: ownsServices(org) && org.mail.mode !== 'off',
436
+ now: () => this.now().getTime(),
437
+ });
438
+ }
439
+ catch (e) {
440
+ if (e instanceof NetworkMoveBackupMissingError) {
441
+ await this.refuse(plan, 'network move without a fresh backup');
442
+ }
443
+ throw e;
444
+ }
445
+ }
397
446
  if (planNeedsConfirmation(plan.steps) &&
398
447
  confirmedOrgName.trim() !== org.displayName.trim()) {
399
448
  await this.refuse(plan, "destructive or migrating step without the org's name typed correctly");
400
449
  throw new ConfirmationRequiredError(orgId);
401
450
  }
402
- const applied = await this.runSteps(org, plan, detail, fence);
451
+ const applied = await this.runSteps(org, plan, detail, fence, backupId);
403
452
  await this.refreshPlatformFilesShare();
404
453
  return applied;
405
454
  }));
@@ -494,6 +543,27 @@ export class FoundryPlanApply {
494
543
  await this.refusePlatform(plan, platform.ownerOrgId, `version change without confirmUpgrade naming: ${unconfirmed.map((u) => u.service).join(', ')}`);
495
544
  throw new UpgradeConfirmationRequiredError(unconfirmed);
496
545
  }
546
+ // F6: before step one. No override. The platform records no applied versions, so no backup id is kept.
547
+ if (this.backupGate) {
548
+ try {
549
+ await requireFreshBackupsForUpgrades(freshPlan, options.confirmUpgrade, {
550
+ coolify: this.backupGate.coolify,
551
+ orgId: platform.ownerOrgId,
552
+ recordedIds: await this.requirePlatformDeps().store.getServiceIds(),
553
+ now: () => this.now().getTime(),
554
+ scope: {
555
+ keys: PLATFORM_BACKUP_DATABASE_SCOPE_KEYS,
556
+ forService: platformDatabasesForService,
557
+ },
558
+ });
559
+ }
560
+ catch (e) {
561
+ if (e instanceof UpgradeBackupMissingError) {
562
+ await this.refusePlatform(plan, platform.ownerOrgId, `upgrade of ${e.service} without a fresh backup`);
563
+ }
564
+ throw e;
565
+ }
566
+ }
497
567
  if (planNeedsConfirmation(plan.steps) && confirmedName.trim() !== 'platform') {
498
568
  await this.refusePlatform(plan, platform.ownerOrgId, "destructive or migrating step without 'platform' typed correctly");
499
569
  throw new PlatformConfirmationRequiredError();
@@ -587,6 +657,11 @@ export class FoundryPlanApply {
587
657
  // gone" itself.
588
658
  await this.destroyer.destroy(target, { org, recordedIds: { ...serviceIds } });
589
659
  delete serviceIds[destroyServiceIdKey(target)];
660
+ if (target.system === 'cloudflare' && target.kind === 'tunnel') {
661
+ // The tunnel is gone, so what was written into it is too.
662
+ delete serviceIds[CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY];
663
+ delete serviceIds[CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY];
664
+ }
590
665
  if (target.system === 'cloudflare' && target.kind === 'worker') {
591
666
  // #318: its partial-deploy traces too, or a retry after a later
592
667
  // step fails would read this Worker as live again.
@@ -807,15 +882,17 @@ export class FoundryPlanApply {
807
882
  at: this.now(),
808
883
  });
809
884
  }
810
- async runSteps(org, plan, detail, fence) {
885
+ async runSteps(org, plan, detail, fence, backupId = null) {
811
886
  const orgId = plan.orgId;
812
887
  const previous = await this.store.getAppliedState(orgId);
813
888
  const serviceIds = { ...(previous?.serviceIds ?? {}) };
814
889
  const checkpoints = [];
815
890
  // Backed by the very `serviceIds` map this method persists once, at the
816
891
  // end — see `StepExecutionContext`'s doc comment for why.
892
+ const ownerCompany = (await this.platformDeps?.store.get())?.ownerOrgId === orgId;
817
893
  const context = {
818
894
  org,
895
+ ownerCompany,
819
896
  recordedIds: () => ({ ...serviceIds }),
820
897
  now: () => this.now().getTime(),
821
898
  recordScopeId: (key, id) => {
@@ -902,6 +979,7 @@ export class FoundryPlanApply {
902
979
  service: orgId,
903
980
  ref: lastApplied.declaredOrgVersion,
904
981
  appliedAt: lastApplied.at,
982
+ backupId,
905
983
  });
906
984
  await this.audit.recordApply({
907
985
  actor: this.actor,
@@ -942,7 +1020,7 @@ export class FoundryPlanApply {
942
1020
  const platformHostnames = await this.platformServiceHostnames();
943
1021
  const recorded = applied?.serviceIds ?? {};
944
1022
  // Read only for an org Agora may switch and whose login host is applied.
945
- const issuerHost = agoraIssuerHostFor(org, isPlatformOwnOrg(org, platformReadiness.platformOwnerOrgId)).host;
1023
+ const issuerHost = agoraIssuerHostFor(org).host;
946
1024
  const loginHostServesDiscovery = issuerHost !== null && loginHostServesAgora(issuerHost, org, recorded)
947
1025
  ? await readLoginHostDiscovery(this.loginHostDiscovery, issuerHost)
948
1026
  : false;
@@ -952,7 +1030,9 @@ export class FoundryPlanApply {
952
1030
  loginHostBindings: this.loginHostBindings,
953
1031
  loginHostServesDiscovery,
954
1032
  zitadelIdTokenUserinfo: this.zitadelIdTokenUserinfo,
955
- broker: this.broker,
1033
+ broker: ownsServices(org) && this.hostOwnServices && this.executor.ownBrokerPort
1034
+ ? await this.executor.ownBrokerPort(org, recorded)
1035
+ : this.broker,
956
1036
  edgeWorkers: this.edgeWorkers,
957
1037
  ownerInviteMail: this.ownerInviteMail,
958
1038
  otherSlugs,
@@ -960,17 +1040,39 @@ export class FoundryPlanApply {
960
1040
  archonUrl,
961
1041
  platformHostnames,
962
1042
  ownServices: this.hostOwnServices,
1043
+ // The `oikos.host` zone, its wildcard and its tunnel are the platform's `platform:oikos-ingress` step.
1044
+ oikosZoneReady: platformReadiness.oikosIngressApplied === true,
963
1045
  ownMailKeyHeld: await this.ownMailKeyHeld(org),
964
1046
  ownMailKeyMintable: this.executor.ownMailKeyMintable?.() ?? false,
1047
+ ...(this.backupGate
1048
+ ? {
1049
+ backupStorageUuid: (await this.platformDeps?.store.getServiceIds())?.[PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY] || null,
1050
+ }
1051
+ : {}),
965
1052
  ownJobsTokenHeld: resumable.jobs,
966
1053
  ownSfuKeysHeld: resumable.sfu,
967
- ...(ownsServices(org) ? { mailRelayApplied: await this.mailRelayApplied() } : {}),
1054
+ ...(ownsServices(org)
1055
+ ? {
1056
+ mailRelayApplied: await this.mailRelayApplied(),
1057
+ ownMailSmtpPort: await this.ownMailSmtpPort(org),
1058
+ mailRelayRoute: await this.mailRelayHoldsRoute(org),
1059
+ }
1060
+ : {}),
968
1061
  sfuPortsOpen,
969
1062
  ownFilesDeps: this.executor.ownFilesDeps?.(),
970
1063
  ...(ownsServices(org) && this.executor.ownFilesR2Ports
971
1064
  ? { ownFilesR2Ports: await this.executor.ownFilesR2Ports(org, recorded) }
972
1065
  : {}),
973
1066
  });
1067
+ if (!ownsServices(org)) {
1068
+ // No company runs the shared services; each runs its own.
1069
+ detail.plannedSteps = [];
1070
+ detail.versionChanges = [];
1071
+ detail.unmanaged.push({
1072
+ description: `${org.slug} has no services.own: the shared services are removed, so a company runs its own. Set services.own.`,
1073
+ key: 'services:shared-removed',
1074
+ });
1075
+ }
974
1076
  if (ownsServices(org) && !this.hostOwnServices) {
975
1077
  detail.unmanaged.push({
976
1078
  description: `${org.slug} runs its own services, and this host has no ownServices port`,
@@ -1046,7 +1148,9 @@ export class FoundryPlanApply {
1046
1148
  const held = this.platformDeps?.heldKeys;
1047
1149
  const platform = await this.platformDeps?.store.get();
1048
1150
  const readiness = orgPlatformReadinessFor(org, base, orgRecordedIds, held ? await heldOwnKeyRefs(held, org) : new Set());
1049
- return platform ? { ...readiness, serviceGitRefs: serviceGitRefsOf(platform) } : readiness;
1151
+ return platform
1152
+ ? { ...readiness, serviceGitRefs: serviceGitRefsOf(platform, org.versionPin) }
1153
+ : readiness;
1050
1154
  }
1051
1155
  /** An own org's Lettermint key is held. A resolver that throws counts as not held. */
1052
1156
  async ownMailKeyHeld(org) {
@@ -1064,6 +1168,27 @@ export class FoundryPlanApply {
1064
1168
  const heldRefs = await heldOwnKeyRefs(held, org);
1065
1169
  return { jobs: heldRefs.has(refs.jobsAdminToken), sfu: heldRefs.has(refs.livekitKeys) };
1066
1170
  }
1171
+ /**
1172
+ * The host port an own org's Stalwart publishes for the relay: the one it already has recorded, else
1173
+ * the lowest port in the range no other org has recorded. Two plans made at the same moment could pick
1174
+ * the same port; the second Stalwart then fails to bind it and the step fails, with nothing recorded.
1175
+ */
1176
+ async ownMailSmtpPort(org) {
1177
+ const own = (await this.store.getAppliedState(org.id))?.serviceIds?.[OWN_MAIL_SMTP_PORT_SCOPE_KEY];
1178
+ if (own && isOwnMailSmtpPort(Number(own)))
1179
+ return Number(own);
1180
+ const taken = new Set();
1181
+ for (const other of await this.store.list()) {
1182
+ const port = (await this.store.getAppliedState(other.id))?.serviceIds?.[OWN_MAIL_SMTP_PORT_SCOPE_KEY];
1183
+ if (port)
1184
+ taken.add(Number(port));
1185
+ }
1186
+ for (let port = OWN_MAIL_SMTP_PORT_BASE; port <= OWN_MAIL_SMTP_PORT_MAX; port += 1) {
1187
+ if (!taken.has(port))
1188
+ return port;
1189
+ }
1190
+ return OWN_MAIL_SMTP_PORT_MAX + 1;
1191
+ }
1067
1192
  /** The relay is declared and has recorded the routes it last applied: port 25 reaches the mail servers. */
1068
1193
  async mailRelayApplied() {
1069
1194
  const platform = await this.platformDeps?.store.get();
@@ -1072,6 +1197,21 @@ export class FoundryPlanApply {
1072
1197
  const recorded = await this.platformDeps?.store.getServiceIds();
1073
1198
  return Boolean(recorded?.[PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY]);
1074
1199
  }
1200
+ /**
1201
+ * The relay's last applied routes (a fingerprint, recorded when it deploys) are the routes the
1202
+ * orgs' recorded SMTP addresses give now, and one of them is this org's mail domain.
1203
+ */
1204
+ async mailRelayHoldsRoute(org) {
1205
+ const platform = await this.platformDeps?.store.get();
1206
+ if (!platform?.mail.relay)
1207
+ return false;
1208
+ const { mailRoutes } = await this.mailRelayInputs();
1209
+ const domain = ownHostnames(org).mailDomain.toLowerCase();
1210
+ if (!mailRoutes.some((r) => r.domain.toLowerCase() === domain))
1211
+ return false;
1212
+ const recorded = await this.platformDeps?.store.getServiceIds();
1213
+ return recorded?.[PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY] === mailRelayFingerprint(mailRoutes);
1214
+ }
1075
1215
  async sharedPlatformReadiness() {
1076
1216
  if (!this.platformDeps)
1077
1217
  return DEFAULT_ORG_PLATFORM_READINESS;
@@ -1097,8 +1237,13 @@ export class FoundryPlanApply {
1097
1237
  catch {
1098
1238
  // not held
1099
1239
  }
1240
+ const ownerOrg = await this.store.get(platform.ownerOrgId);
1241
+ const ownerRecorded = ownerOrg
1242
+ ? (await this.store.getAppliedState(ownerOrg.id))?.serviceIds
1243
+ : {};
1100
1244
  return {
1101
1245
  ...readiness,
1246
+ platformOwnerUserId: platformOwnerUserIdFrom(ownerOrg, ownerRecorded ?? {}),
1102
1247
  mailKeyHeld,
1103
1248
  canHoldSecrets: typeof heldKeys?.put === 'function',
1104
1249
  dnsBlocked: dnsBlockedReason(platform, recorded),
@@ -1166,52 +1311,55 @@ export class FoundryPlanApply {
1166
1311
  const callsOrgSlugs = (await this.store.list())
1167
1312
  .filter((org) => org.calls.enabled)
1168
1313
  .map((org) => org.slug);
1169
- const mailRelay = platform.mail.relay === true ? await this.mailRelayInputs(platform) : null;
1314
+ const mailRelay = platform.mail.relay === true ? await this.mailRelayInputs() : null;
1315
+ const owner = await this.platformOwnerInput(platform);
1170
1316
  const detail = computePlatformPlanDetail(platform, live, {
1317
+ owner,
1171
1318
  jobsObservable: readers.coolify !== undefined,
1172
1319
  sfuBlocked,
1173
1320
  callsOrgSlugs,
1174
1321
  ...(mailRelay ?? {}),
1175
1322
  recordedIds: serviceIds,
1176
- blocked: (await executor.blockedSteps?.(platform, serviceIds)) ?? {},
1177
- drifted: (await executor.driftedSteps?.(platform, serviceIds)) ?? {},
1323
+ liveBuilds: await readLiveBuilds(readers.coolify, serviceIds),
1324
+ blocked: (await executor.blockedSteps?.(platform, serviceIds, owner)) ?? {},
1325
+ drifted: (await executor.driftedSteps?.(platform, serviceIds, owner)) ?? {},
1178
1326
  });
1179
1327
  return { platform, plan: toPlatformPlan(detail, platform.ownerOrgId), detail };
1180
1328
  }
1329
+ /** The owner company's own sign-in and files host, read from its applied ids; `null` members until applied. */
1330
+ async platformOwnerInput(platform) {
1331
+ const owner = await this.store.get(platform.ownerOrgId);
1332
+ if (!owner)
1333
+ return { signIn: null, files: null };
1334
+ const recorded = (await this.store.getAppliedState(owner.id))?.serviceIds ?? {};
1335
+ return { signIn: ownerSignInFrom(owner, recorded), files: ownerFilesFrom(owner, recorded) };
1336
+ }
1181
1337
  /**
1182
1338
  * What the shared mail relay needs from the orgs. A route exists for an org
1183
1339
  * that owns its services, serves mail on its own domain and has recorded its
1184
1340
  * mail server's SMTP address (`mail_smtp_internal`, written by the company
1185
- * mail step). Every other org with mail on its own domain, the platform
1186
- * apex and the shared no-domain apex are platform domains: the platform's Stalwart receives them. An org
1187
- * that owns its services but has no mail server yet appears in neither list,
1188
- * so the relay refuses its mail until the server exists.
1341
+ * mail step), on its mail domain: its own domain, or `<slug>.oikos.host`.
1342
+ * There is no platform mail server: a domain with no route is refused. An org
1343
+ * that owns its services but has no mail server yet has no route, so the
1344
+ * relay refuses its mail until the server exists.
1189
1345
  */
1190
- async mailRelayInputs(platform) {
1346
+ async mailRelayInputs() {
1191
1347
  const candidates = [];
1192
- const mailPlatformDomains = [platform.apex];
1193
1348
  for (const org of await this.store.list()) {
1194
1349
  if (org.mail.mode === 'off')
1195
1350
  continue;
1196
- // Mail without the org's own domain is served on the shared no-domain apex.
1197
- if (!org.domain || !org.mail.usesDomain) {
1198
- if (!mailPlatformDomains.includes(NO_DOMAIN_MAIL_APEX)) {
1199
- mailPlatformDomains.push(NO_DOMAIN_MAIL_APEX);
1200
- }
1201
- continue;
1202
- }
1203
- if (!ownsServices(org)) {
1204
- mailPlatformDomains.push(org.domain);
1351
+ if (!ownsServices(org))
1205
1352
  continue;
1206
- }
1207
1353
  const recorded = (await this.store.getAppliedState(org.id))?.serviceIds ?? {};
1208
1354
  const target = recorded[OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY];
1209
1355
  if (recorded.coolify_platform_mail_app && target) {
1210
- candidates.push({ slug: org.slug, route: { domain: org.domain, target } });
1356
+ candidates.push({
1357
+ slug: org.slug,
1358
+ route: { domain: ownHostnames(org).mailDomain, target },
1359
+ });
1211
1360
  }
1212
1361
  }
1213
- // A domain the platform serves, or two companies claim, gets no route; the other routes stay.
1214
- const platformSet = new Set(mailPlatformDomains.map((d) => d.toLowerCase()));
1362
+ // A domain two companies claim gets no route; the other routes stay.
1215
1363
  const claims = new Map();
1216
1364
  for (const { route } of candidates) {
1217
1365
  const domain = route.domain.toLowerCase();
@@ -1221,17 +1369,14 @@ export class FoundryPlanApply {
1221
1369
  const mailRoutesBlocked = [];
1222
1370
  for (const { slug, route } of candidates) {
1223
1371
  const domain = route.domain.toLowerCase();
1224
- if (platformSet.has(domain)) {
1225
- mailRoutesBlocked.push(`${slug}'s mail route for ${route.domain} is blocked, not planned: ${route.domain} is a platform mail domain`);
1226
- }
1227
- else if ((claims.get(domain) ?? 0) > 1) {
1372
+ if ((claims.get(domain) ?? 0) > 1) {
1228
1373
  mailRoutesBlocked.push(`${slug}'s mail route for ${route.domain} is blocked, not planned: another company claims ${route.domain}`);
1229
1374
  }
1230
1375
  else {
1231
1376
  mailRoutes.push(route);
1232
1377
  }
1233
1378
  }
1234
- return { mailRoutes, mailPlatformDomains, mailRoutesBlocked };
1379
+ return { mailRoutes, mailRoutesBlocked };
1235
1380
  }
1236
1381
  async sfuBlockedReason(platform) {
1237
1382
  if (!platform.sfu.enabled)
@@ -1272,6 +1417,7 @@ export class FoundryPlanApply {
1272
1417
  const serviceIds = { ...(await store.getServiceIds()) };
1273
1418
  const context = {
1274
1419
  platform,
1420
+ owner: await this.platformOwnerInput(platform),
1275
1421
  recordedIds: () => ({ ...serviceIds }),
1276
1422
  recordScopeId: (key, id) => {
1277
1423
  serviceIds[key] = id;
@@ -4,6 +4,7 @@ import type { DeclaredPlatform } from '../platform.js';
4
4
  import type { PlannedStep } from './diff.js';
5
5
  import type { LiveResource } from './live.js';
6
6
  import { type MailRoute, PLATFORM_MAIL_RELAY_KEY } from './mail-relay.js';
7
+ import type { OwnerFiles, OwnerSignIn, PlatformOwnerInput } from './platform-org.js';
7
8
  /**
8
9
  * The platform's own diff result, the {@link DeclaredPlatform} counterpart to
9
10
  * `diff.ts`'s `PlanDetail` -- everything the same, minus the `orgId` an org's
@@ -185,11 +186,15 @@ export declare const PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY = "archon_applie
185
186
  */
186
187
  export declare const PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY = "archon_applied_rolling_update";
187
188
  /**
188
- * The sign-in org Archon accepts: the platform owner company's own
189
- * ({@link PLATFORM_OWNER_ORG_SCOPE_KEY}), else the platform's `foundry` org for
190
- * a platform with no owner company declared. Undefined when neither is recorded.
189
+ * The owner company's sign-in hostname Archon was last registered under, and
190
+ * the owner's files host (`<internalUrl>|<audience>`, or `none`) its env was
191
+ * last written with. `planArchon` plans the step again when either differs
192
+ * from the owner's current ones (a marker never recorded reads as `none`).
191
193
  */
192
- export declare function archonAuthOrganizationId(recorded: Readonly<Record<string, string>>): string | undefined;
194
+ export declare const PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY = "archon_applied_auth_hostname";
195
+ export declare const PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY = "archon_applied_files";
196
+ /** The value {@link PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY} records for the owner's files host. */
197
+ export declare function archonFilesMarker(files: OwnerFiles | null | undefined): string;
193
198
  /**
194
199
  * #175: set to '1' before the step points the tunnel at `http_status:404`
195
200
  * (Access app rebuild), cleared to '' once the real ingress is written back.
@@ -537,8 +542,68 @@ export declare function serviceDbEnvAppliedScopeKey(service: 'chat' | 'calls-ser
537
542
  * made again under a new id, is repaired once.
538
543
  */
539
544
  export declare function zitadelAppIdTokenUserinfoAppliedScopeKey(appKey: string): string;
540
- /** The ZITADEL app id of Archon's sign-in client in the platform's own ZITADEL. */
545
+ /**
546
+ * Archon's own sign-in organisation, in the owner company's ZITADEL instance
547
+ * and apart from the organisation that holds the company's people (and Boule's
548
+ * sign-in). Made by the `zitadel:archon-org` step, which runs before
549
+ * `platform:archon`. The first six keys below are the step's own progress:
550
+ * each is recorded only after its call succeeded.
551
+ */
552
+ export declare const PLATFORM_ARCHON_ORG_SCOPE_KEY = "zitadel_platform_archon_org";
553
+ /** The primary domain registered on Archon's sign-in organisation. */
554
+ export declare const PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY = "zitadel_platform_archon_org_domain";
555
+ /** The domain the sign-in server refused to make primary; set instead of failing, reported as blocked. */
556
+ export declare const PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY = "zitadel_platform_archon_org_domain_blocked";
557
+ /** The Archon org id once self-registration is closed on it. */
558
+ export declare const PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY = "zitadel_platform_archon_org_registration_closed";
559
+ /** The Archon org id once its login policy forces a second factor. */
560
+ export declare const PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY = "zitadel_platform_archon_org_policy";
561
+ /** The first person's user id in the Archon org (`none` when no owner is declared), recorded once the invite was mailed. */
562
+ export declare const PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY = "zitadel_platform_archon_owner_invite";
563
+ /** The Archon org the recorded invite was made in. */
564
+ export declare const PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY = "zitadel_platform_archon_owner_invite_org";
565
+ /** The name of the Archon sign-in organisation: `<owner slug>-archon`. */
566
+ export declare function archonOrgName(ownerSlug: string): string;
567
+ /** The primary domain of the Archon sign-in organisation: `archon.` before the owner's own sign-in domain. */
568
+ export declare function archonOrgDomain(ownerSignInDomain: string): string;
569
+ /**
570
+ * The CI organisation id the last Archon apply wrote as `CI_ORGANIZATION_ID`,
571
+ * and the `ARCHON_FIRST_OWNER_LINK` value it wrote (empty when none).
572
+ * `planArchon` plans the step again when either differs from what is wanted.
573
+ */
574
+ export declare const PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY = "archon_applied_ci_org";
575
+ export declare const PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY = "archon_applied_first_owner_link";
576
+ /**
577
+ * `<archonUserId>:<ciUserId>`: the first person in both organisations, which
578
+ * Archon seeds its link table from. Empty when either id is not known (no
579
+ * owner declared, or the CI-side account not recorded yet).
580
+ */
581
+ export declare function archonFirstOwnerLink(recordedIds: Readonly<Record<string, string>>, ownerSignIn: OwnerSignIn | null): string;
582
+ /**
583
+ * The one-time-PIN provider id the Archon Access app was last made with.
584
+ * Drift is read against it, because a plan must not ask edge to find or make
585
+ * the provider.
586
+ */
587
+ export declare const PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY = "cloudflare_platform_archon_access_pin";
588
+ /**
589
+ * The Archon org the `AUTH_APP_KEY` in Archon's env was minted for. A key
590
+ * minted for another org (the CI org, before the move) is replaced by a fresh
591
+ * one, so the key the CI-org broker entry held stops being Archon's.
592
+ */
593
+ export declare const PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY = "archon_app_key_org";
594
+ /**
595
+ * `<org>|<project>|<app>` of Archon's old sign-in client while it still waits
596
+ * to be deleted. Set before the new client's ids overwrite the recorded ones,
597
+ * cleared once the old client is gone.
598
+ */
599
+ export declare const PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY = "zitadel_platform_archon_retired_client";
600
+ /** The ZITADEL app id of Archon's sign-in client in the owner company's ZITADEL. */
541
601
  export declare const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = "zitadel_platform_archon_app";
602
+ /** The ZITADEL project id Archon's sign-in client lives in. */
603
+ export declare const PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY = "zitadel_platform_archon_project";
604
+ /** The ZITADEL project and client ids of the Access sign-in provider, in the owner company's ZITADEL. */
605
+ export declare const PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY = "zitadel_platform_access_project";
606
+ export declare const PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY = "zitadel_platform_access_app";
542
607
  /**
543
608
  * The ZITADEL app id of Archon's sign-in client once "user info inside the
544
609
  * ID token" was switched on for it. `planArchonIdTokenUserinfo` plans the
@@ -587,9 +652,8 @@ export declare function appDomainsAppliedScopeKey(entryKey: string): string;
587
652
  * unknown, bare before 4b2380f and `https://` after it.
588
653
  */
589
654
  export declare function appliedAppDomains(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, entryKey: string): string[];
590
- /** Whether the secret sources can be filled right now: the mail key is held, and a new shared secret can be held. */
655
+ /** Whether the secret sources can be filled right now: a new shared secret can be held. */
591
656
  export interface AppSecretAvailability {
592
- platformMailKey: boolean;
593
657
  sharedSecret: boolean;
594
658
  }
595
659
  /**
@@ -608,7 +672,7 @@ export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, rec
608
672
  unresolved: Array<[string, string]>;
609
673
  secrets: Array<{
610
674
  name: string;
611
- source: 'platformMailKey' | EnvSharedSecret;
675
+ source: EnvSharedSecret;
612
676
  }>;
613
677
  unresolvedSecrets: Array<[string, string]>;
614
678
  marker: Record<string, string>;
@@ -627,6 +691,13 @@ export declare const PLATFORM_AI_KEY = "platform:ai";
627
691
  * public hostname. Holds whichever way the public-hostname decision goes.
628
692
  */
629
693
  export declare const PLATFORM_AI_INTERNAL_URL_SCOPE_KEY = "ai_platform_internal_url";
694
+ /**
695
+ * The id of the tunnel that carries the AI host's public hostname. Recorded once the tunnel, its
696
+ * connector and the DNS record are in place; an entry is given `AI_URL` only after it.
697
+ */
698
+ export declare const PLATFORM_AI_TUNNEL_SCOPE_KEY = "cloudflare_platform_ai_tunnel";
699
+ /** The one Coolify app that runs `cloudflared` for the AI tunnel. */
700
+ export declare const PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY = "coolify_platform_ai_connector_app";
630
701
  /**
631
702
  * #172: the same two-key discipline as the ZITADEL admin key above, for
632
703
  * foundry's AI management key (the bearer key `ai:organisation`, `ai:budget`
@@ -833,6 +904,18 @@ export declare const PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY = "hc_applied_git_ref
833
904
  * and that implementation is where the key is actually used.
834
905
  */
835
906
  export declare const PLATFORM_HC_API_KEY_REF = "hc:platform:apiKey";
907
+ /**
908
+ * foundry#544: the held-key ref a human puts the *outside* Healthchecks
909
+ * account's project API key under, read only while
910
+ * `platform.heartbeatsProvider` is `'hosted'` -- {@link PLATFORM_HC_API_KEY_REF}'s
911
+ * own ref is for the self-hosted instance only, and the two are never
912
+ * conflated: switching provider needs its own key held under its own ref,
913
+ * never the other one silently reused. Same "this package never reads the
914
+ * value back" rule as {@link PLATFORM_HC_API_KEY_REF}'s own doc comment --
915
+ * whoever implements {@link HeartbeatsPort} against the outside service is
916
+ * the host.
917
+ */
918
+ export declare const PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF = "heartbeats:platform:hostedApiKey";
836
919
  /**
837
920
  * #540: the heartbeat check named after the uptime Worker (foundry#542,
838
921
  * which is what actually pings it -- blocked today on edge#26, so nothing
@@ -989,16 +1072,14 @@ export interface PlanPlatformOptions {
989
1072
  * `platform.mail.relay` is true (default `[]`).
990
1073
  */
991
1074
  mailRoutes?: readonly MailRoute[];
992
- /**
993
- * The domains the platform's own Stalwart serves; the relay hands them to
994
- * it and refuses any domain in neither list. Used only when
995
- * `platform.mail.relay` is true (default `[]`).
996
- */
997
- mailPlatformDomains?: readonly string[];
998
1075
  /** One line per company route left out of `mailRoutes`; each is reported blocked and the relay still plans without it. */
999
1076
  mailRoutesBlocked?: readonly string[];
1077
+ /** The owner company's own sign-in and files host; Archon and the Access provider read them (default: neither applied). */
1078
+ owner?: PlatformOwnerInput;
1000
1079
  /** The platform's recorded resource/scope ids (default `{}`). */
1001
1080
  recordedIds?: Readonly<Record<string, string>>;
1081
+ /** The commit each recorded app runs now, by app name; a differing one gives a `build-drift:<name>` note (default `{}`). */
1082
+ liveBuilds?: Readonly<Record<string, string>>;
1002
1083
  /** Per-step block reasons the executor reports (default `{}`). */
1003
1084
  blocked?: {
1004
1085
  auth?: string;
@@ -1032,6 +1113,8 @@ export interface PlanPlatformOptions {
1032
1113
  edge?: string;
1033
1114
  /** #604: why the Access sign-in provider cannot be made yet (no dependencies, or edge cannot confirm Access is ready), or absent. */
1034
1115
  accessProvider?: string;
1116
+ /** Why the shared oikos.host ingress cannot be made (no dependencies, or its zone is not on edge's account), or absent. */
1117
+ oikosIngress?: string;
1035
1118
  /**
1036
1119
  * #257: true once a service's database exists but the host built the
1037
1120
  * platform executor with no backup dependencies -- every such service's
@@ -1095,6 +1178,10 @@ export interface PlanPlatformOptions {
1095
1178
  * step is not planned.
1096
1179
  */
1097
1180
  authSignInPolicy?: boolean;
1181
+ /** Why Archon's own sign-in organisation cannot be made (no dependencies, no ZITADEL client for the instance), or absent. */
1182
+ archonOrg?: string;
1183
+ /** Why the first person cannot be invited into Archon's organisation yet (no work address, no mail port). The rest of the step still plans. */
1184
+ archonOrgInvite?: string;
1098
1185
  };
1099
1186
  /**
1100
1187
  * Steps whose live state differs from the declared one in a way the recorded
@@ -1189,11 +1276,8 @@ export declare const JOBS_INTERNAL_URL_SCOPE_KEY = "jobs_internal_url";
1189
1276
  * never a wrong step.
1190
1277
  */
1191
1278
  export declare const PLATFORM_BACKUP_DATABASE_SCOPE_KEYS: {
1192
- readonly jobs: "coolify_jobs_database";
1193
- readonly auth: "coolify_platform_auth_database";
1194
1279
  readonly archon: "coolify_platform_archon_database";
1195
1280
  readonly ai: "coolify_platform_ai_database";
1196
- readonly files: "coolify_platform_files_database";
1197
1281
  readonly integrations: "coolify_integrations_database";
1198
1282
  };
1199
1283
  export type PlatformBackupServiceKey = keyof typeof PLATFORM_BACKUP_DATABASE_SCOPE_KEYS;
@@ -1265,3 +1349,35 @@ export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = "in
1265
1349
  * `''`) as soon as the mint is recorded, so one request is one rotation.
1266
1350
  */
1267
1351
  export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = "integrations_management_key_rotation_requested";
1352
+ /**
1353
+ * #175: Archon (https://github.com/wtfalch/archon), the one shared console,
1354
+ * built fresh at `platform.archon.hostname` (`archon.<apex>`) rather than the
1355
+ * hand-deployed `operator` app it replaces (#165). One step,
1356
+ * `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, covers the app, its
1357
+ * database, its Cloudflare tunnel, Access app and DNS record, and its sign-in
1358
+ * client in the platform's own ZITADEL -- the same bundling {@link planAuth}
1359
+ * does, for the same reason.
1360
+ *
1361
+ * Idempotent on the recorded resource id plus the applied hostname and
1362
+ * `gitRef`, for the reason {@link planAuth} gives: no platform reader reports
1363
+ * these resources, so a live check would replan the step forever. A changed
1364
+ * hostname or `gitRef` plans the step again, and so does `drifted` (the Access
1365
+ * policy's include list is not the declared one). A re-run mints nothing.
1366
+ *
1367
+ * Blocked, not planned, while the platform's sign-in is neither recorded nor
1368
+ * planned in this same plan: Archon's sign-in client is created inside it, so
1369
+ * the step could only throw.
1370
+ *
1371
+ * #213: also puts {@link PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF},
1372
+ * {@link PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF} and
1373
+ * {@link PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF} as held keys, before
1374
+ * the app deploys -- `putArchonMasterKeys` in `platform-providers.ts`.
1375
+ *
1376
+ * #197: a recorded app with no recorded `cloudflared` connector
1377
+ * ({@link PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY}) plans the step again, as
1378
+ * {@link planAuth} does.
1379
+ */
1380
+ /** Archon signs its operators in through the owner company's own sign-in; fixed text, part of the stale-plan comparison. */
1381
+ export declare const ARCHON_OWNER_SIGN_IN_BLOCKED = "Archon is blocked, not planned: the owner company's sign-in is not applied yet";
1382
+ /** Archon signs in from its own organisation, which the `zitadel:archon-org` step makes; fixed text, part of the stale-plan comparison. */
1383
+ export declare const ARCHON_ORG_BLOCKED = "Archon is blocked, not planned: its own sign-in organisation is not applied yet";