@wtfalch/foundry 0.34.0 → 0.35.1
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.
- package/dist/bootstrap/bootstrap.d.ts +1 -1
- package/dist/bootstrap/bootstrap.js +49 -11
- package/dist/bootstrap/declare.d.ts +12 -5
- package/dist/bootstrap/declare.js +33 -18
- package/dist/bootstrap/versions.d.ts +20 -0
- package/dist/bootstrap/versions.js +46 -0
- package/dist/calls-sfu.js +3 -3
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/migrations/0053_heartbeats_provider.sql +9 -0
- package/dist/migrations/0053_org_version_pin.sql +6 -0
- package/dist/org.d.ts +60 -14
- package/dist/org.js +118 -28
- package/dist/plan/backup-gate.d.ts +93 -0
- package/dist/plan/backup-gate.js +261 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +20 -3
- package/dist/plan/build-record.d.ts +56 -0
- package/dist/plan/build-record.js +109 -0
- package/dist/plan/calls-matrix.js +3 -3
- package/dist/plan/cloudflared-connector.js +2 -2
- package/dist/plan/company-backups.d.ts +31 -0
- package/dist/plan/company-backups.js +101 -0
- package/dist/plan/deploy-health.d.ts +12 -2
- package/dist/plan/deploy-health.js +3 -1
- package/dist/plan/destinations.d.ts +73 -0
- package/dist/plan/destinations.js +217 -0
- package/dist/plan/diff.d.ts +17 -8
- package/dist/plan/diff.js +131 -131
- package/dist/plan/edge-access.d.ts +15 -0
- package/dist/plan/edge-access.js +21 -2
- package/dist/plan/executor.d.ts +166 -7
- package/dist/plan/executor.js +547 -106
- package/dist/plan/health-targets.d.ts +1 -3
- package/dist/plan/health-targets.js +36 -36
- package/dist/plan/held-key-refs.d.ts +5 -0
- package/dist/plan/held-key-refs.js +5 -0
- package/dist/plan/hostnames.d.ts +10 -0
- package/dist/plan/hostnames.js +14 -0
- package/dist/plan/index.d.ts +5 -0
- package/dist/plan/index.js +3 -0
- package/dist/plan/live.d.ts +6 -0
- package/dist/plan/live.js +3 -2
- package/dist/plan/locked-hostnames.d.ts +2 -1
- package/dist/plan/locked-hostnames.js +16 -3
- package/dist/plan/login-host.d.ts +9 -20
- package/dist/plan/login-host.js +10 -25
- package/dist/plan/mail-relay.d.ts +48 -29
- package/dist/plan/mail-relay.js +145 -68
- package/dist/plan/network-move.d.ts +20 -0
- package/dist/plan/network-move.js +41 -0
- package/dist/plan/oikos-ingress.d.ts +111 -0
- package/dist/plan/oikos-ingress.js +231 -0
- package/dist/plan/org-names.d.ts +8 -2
- package/dist/plan/org-names.js +57 -1
- package/dist/plan/own-diff.d.ts +9 -8
- package/dist/plan/own-diff.js +42 -34
- package/dist/plan/own-hostnames.d.ts +27 -0
- package/dist/plan/own-hostnames.js +24 -0
- package/dist/plan/own-limits.d.ts +198 -7
- package/dist/plan/own-limits.js +163 -32
- package/dist/plan/own-services.js +3 -1
- package/dist/plan/own-sfu.js +3 -2
- package/dist/plan/plan-apply.d.ts +47 -7
- package/dist/plan/plan-apply.js +184 -38
- package/dist/plan/platform-diff.d.ts +133 -17
- package/dist/plan/platform-diff.js +231 -68
- package/dist/plan/platform-org.d.ts +79 -10
- package/dist/plan/platform-org.js +93 -13
- package/dist/plan/platform-providers.d.ts +118 -73
- package/dist/plan/platform-providers.js +770 -231
- package/dist/plan/proxy-labels.d.ts +57 -11
- package/dist/plan/proxy-labels.js +94 -14
- package/dist/plan/run-options.d.ts +19 -0
- package/dist/plan/run-options.js +46 -0
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +76 -8
- package/dist/platform.d.ts +32 -0
- package/dist/platform.js +16 -0
- package/dist/provision.d.ts +2 -0
- package/dist/provision.js +3 -1
- package/dist/slug.d.ts +3 -1
- package/dist/slug.js +25 -3
- package/dist/store/postgres.js +43 -1
- package/dist/store/schema.d.ts +36 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -3
- 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
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return
|
|
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
|
-
/**
|
|
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 = {
|
|
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 = [],
|
|
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'),
|
|
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
|
|
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 !==
|
|
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 ${
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
2807
|
-
recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ===
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
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 (!
|
|
2843
|
-
return {
|
|
2844
|
-
|
|
2845
|
-
|
|
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
|
|
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
|
|
80
|
-
* `AI_URL`. `null` or absent until the platform has
|
|
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
|
|
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
|
|
164
|
-
*
|
|
165
|
-
* owner to invite. `null` or absent until recorded.
|
|
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;
|