@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.
- 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 +799 -241
- 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
package/dist/plan/diff.js
CHANGED
|
@@ -4,20 +4,24 @@ import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPEC
|
|
|
4
4
|
import { BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_MAIL_TOKEN_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
6
|
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
7
|
+
import { planCompanyBackups } from './company-backups.js';
|
|
8
|
+
import { onPlatformNetwork } from './destinations.js';
|
|
7
9
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
8
10
|
import { planEdgeReservedLabels, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
9
11
|
import { planFilesKeys } from './entry-files-key.js';
|
|
10
12
|
import { hasWildcardGate, planGates } from './gates.js';
|
|
11
|
-
import { appHostname,
|
|
13
|
+
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
12
14
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
13
|
-
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor,
|
|
15
|
+
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
14
16
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
17
|
+
import { hasAdoptedResources, networkMoveNeeded, planNetworkMove } from './network-move.js';
|
|
15
18
|
import { OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, planOwnSfu, } from './own-diff.js';
|
|
16
19
|
import { ownDnsHostnames } from './own-dns.js';
|
|
20
|
+
import { ownHostnames } from './own-hostnames.js';
|
|
17
21
|
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
18
22
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
19
23
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
20
|
-
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec } from './proxy-labels.js';
|
|
24
|
+
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryRoutes, entryRoutesMarker, } from './proxy-labels.js';
|
|
21
25
|
import { ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY, ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, signInDomainBlockedText, signInOrgDomain, } from './sign-in-org.js';
|
|
22
26
|
import { withStepSystem } from './step-system.js';
|
|
23
27
|
import { planWorkers } from './worker.js';
|
|
@@ -182,26 +186,22 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
182
186
|
const byKey = new Map(live.map((r) => [resourceKey(r.system, r.kind, r.key), r]));
|
|
183
187
|
const claimed = new Set();
|
|
184
188
|
const plannedSteps = [];
|
|
185
|
-
// The platform's own org: its domain is the platform apex (the same fact
|
|
186
|
-
// #491 reads for mail). It signs in on the shared instance's own host, so
|
|
187
|
-
// `issuer.usesDomain` never moves it.
|
|
188
|
-
const isPlatformOrg = isPlatformOwnOrg(org, platformReadiness.platformOwnerOrgId);
|
|
189
189
|
// An own org's sign-in comes before every step that signs in through it.
|
|
190
190
|
const ownAuth = planOwnAuth({
|
|
191
191
|
org,
|
|
192
192
|
recordedIds,
|
|
193
193
|
readiness: platformReadiness,
|
|
194
|
-
isPlatformOrg,
|
|
195
194
|
ownServices: options.ownServices ?? false,
|
|
196
195
|
zone: org.domain ? zoneReadiness(org.domain, byKey) : null,
|
|
196
|
+
oikosZoneReady: options.oikosZoneReady ?? false,
|
|
197
197
|
mailKeyHeld: options.ownMailKeyHeld ?? false,
|
|
198
198
|
mailKeyMintable: options.ownMailKeyMintable ?? false,
|
|
199
199
|
});
|
|
200
200
|
if (ownAuth.step)
|
|
201
201
|
plannedSteps.push(ownAuth.step);
|
|
202
202
|
// An own org's ZITADEL is the company's own instance: nothing that needs it plans before it exists.
|
|
203
|
-
const ownSignInPending = ownsServices(org) && !
|
|
204
|
-
const issuer = planIssuer(org, byKey, claimed, platformReadiness.authReady
|
|
203
|
+
const ownSignInPending = ownsServices(org) && !ownAuthApplied(org, recordedIds);
|
|
204
|
+
const issuer = planIssuer(org, byKey, claimed, platformReadiness.authReady);
|
|
205
205
|
if (issuer.step)
|
|
206
206
|
plannedSteps.push(issuer.step);
|
|
207
207
|
// Before the domain and the owners: no invite runs on an org still open to registration.
|
|
@@ -216,12 +216,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
216
216
|
// #267: after the ZITADEL org step, before any app's own issuer
|
|
217
217
|
// application. Apps created later in this run are not in the binding yet;
|
|
218
218
|
// the next run replans this step and repoints them.
|
|
219
|
-
const loginHost = planLoginHost(org, recordedIds, platformReadiness.authReady, issuer.step !== null, options.loginHostBindings ?? false
|
|
220
|
-
// #488: `agoraHost.issuer` is the shared ZITADEL instance's own issuer
|
|
221
|
-
// URL (`https://<platform.auth.hostname>`), the same value every org's
|
|
222
|
-
// sign-in already reads through `planOwners` above -- no separate port
|
|
223
|
-
// or plumbing needed to know the platform's own auth hostname.
|
|
224
|
-
platformAuthHostnameFrom('error' in agoraHost ? null : agoraHost.issuer), isPlatformOrg);
|
|
219
|
+
const loginHost = planLoginHost(org, recordedIds, platformReadiness.authReady, issuer.step !== null, options.loginHostBindings ?? false);
|
|
225
220
|
if (loginHost.step)
|
|
226
221
|
plannedSteps.push(loginHost.step);
|
|
227
222
|
// #444a: the org's one shared Postgres server and its fixed schema
|
|
@@ -233,6 +228,10 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
233
228
|
// is pushed just below, before mail.
|
|
234
229
|
const zone = planZone(org, byKey, claimed);
|
|
235
230
|
const zoneWaiting = zone.blocked !== null;
|
|
231
|
+
// A company without a domain has no zone of its own: it waits for the `oikos.host` zone at the platform's edge.
|
|
232
|
+
const ownZoneActive = org.domain
|
|
233
|
+
? zone.step === null && !zoneWaiting
|
|
234
|
+
: (options.oikosZoneReady ?? false);
|
|
236
235
|
const zoneBlocked = [];
|
|
237
236
|
// #555: steps that write DNS records, which go through edge.
|
|
238
237
|
const dnsBlocked = [];
|
|
@@ -251,14 +250,16 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
251
250
|
org,
|
|
252
251
|
recordedIds,
|
|
253
252
|
readiness: platformReadiness,
|
|
254
|
-
isPlatformOrg,
|
|
255
253
|
ownServices: options.ownServices ?? false,
|
|
256
254
|
mailRelayApplied: options.mailRelayApplied ?? false,
|
|
257
|
-
zoneActive:
|
|
255
|
+
zoneActive: ownZoneActive,
|
|
256
|
+
...(options.ownMailSmtpPort !== undefined ? { smtpPort: options.ownMailSmtpPort } : {}),
|
|
258
257
|
});
|
|
259
258
|
if (ownMail.step)
|
|
260
259
|
plannedSteps.push(ownMail.step);
|
|
261
|
-
const mail = planMail(org, byKey, claimed, recordedIds, platformReadiness.mailHost, platformReadiness.platformApex, platformReadiness.platformLettermintProjectId
|
|
260
|
+
const mail = planMail(org, byKey, claimed, recordedIds, platformReadiness.mailHost, platformReadiness.platformApex, platformReadiness.platformLettermintProjectId, options.mailRelayRoute === false
|
|
261
|
+
? `the mail relay has no route for ${ownHostnames(org).mailDomain} yet: apply the platform`
|
|
262
|
+
: undefined);
|
|
262
263
|
// #238: own-domain mail publishes DNS records into the zone, so it waits for
|
|
263
264
|
// an active zone -- blocked, not planned (rule 2).
|
|
264
265
|
if (mail.step && zoneWaiting && org.mail.usesDomain && org.domain) {
|
|
@@ -277,19 +278,19 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
277
278
|
const mailCopy = planMailCopy(org, byKey, recordedIds);
|
|
278
279
|
if (mailCopy)
|
|
279
280
|
plannedSteps.push(mailCopy);
|
|
281
|
+
const versionPin = org.versionPin;
|
|
280
282
|
const chat = planChat(org, byKey, claimed, recordedIds, agoraHost, issuer.step !== null);
|
|
281
283
|
if (chat.step)
|
|
282
284
|
plannedSteps.push(chat.step);
|
|
283
|
-
const calls = planCalls(org, byKey, claimed, platformReadiness.callsReady);
|
|
285
|
+
const calls = planCalls(org, byKey, claimed, platformReadiness.callsReady, versionPin?.calls);
|
|
284
286
|
if (calls.step)
|
|
285
287
|
plannedSteps.push(calls.step);
|
|
286
288
|
// An own org's media server, before the calls service that reads its addresses.
|
|
287
289
|
const ownSfu = planOwnSfu({
|
|
288
290
|
org,
|
|
289
291
|
recordedIds,
|
|
290
|
-
isPlatformOrg,
|
|
291
292
|
ownServices: options.ownServices ?? false,
|
|
292
|
-
zoneActive:
|
|
293
|
+
zoneActive: ownZoneActive,
|
|
293
294
|
readiness: platformReadiness,
|
|
294
295
|
sfuPortsOpen: options.sfuPortsOpen ?? false,
|
|
295
296
|
keysHeld: options.ownSfuKeysHeld ?? false,
|
|
@@ -320,7 +321,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
320
321
|
const versionChanges = [];
|
|
321
322
|
if (org.calls.enabled && !calls.blocked) {
|
|
322
323
|
const callsAppLive = Boolean(byKey.get(resourceKey('coolify', 'calls', org.id)));
|
|
323
|
-
const synapseChange = versionNotAppliedLine('synapse', callsAppLive, recordedIds[CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.synapse);
|
|
324
|
+
const synapseChange = versionNotAppliedLine('synapse', callsAppLive, recordedIds[CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY], versionPin?.calls ?? VERSIONS.synapse);
|
|
324
325
|
if (synapseChange)
|
|
325
326
|
versionChanges.push(synapseChange);
|
|
326
327
|
const masChange = versionNotAppliedLine('mas', callsAppLive, recordedIds[CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.mas);
|
|
@@ -332,7 +333,6 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
332
333
|
org,
|
|
333
334
|
recordedIds,
|
|
334
335
|
readiness: platformReadiness,
|
|
335
|
-
isPlatformOrg,
|
|
336
336
|
ownServices: options.ownServices ?? false,
|
|
337
337
|
filesDeps: options.ownFilesDeps ?? { keys: false, bucket: false },
|
|
338
338
|
});
|
|
@@ -343,14 +343,13 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
343
343
|
? planOwnFilesR2({
|
|
344
344
|
org,
|
|
345
345
|
recordedIds,
|
|
346
|
-
isPlatformOrg,
|
|
347
346
|
ownServices: options.ownServices ?? false,
|
|
348
347
|
ports: options.ownFilesR2Ports,
|
|
349
348
|
})
|
|
350
349
|
: { step: null, blocked: null };
|
|
351
350
|
if (ownFilesR2.step)
|
|
352
351
|
plannedSteps.push(ownFilesR2.step);
|
|
353
|
-
const filesOrganisation = planFilesOrganisation(org, byKey, claimed, platformReadiness.filesReady, platformReadiness.missingKeys.files, platformReadiness.noKeyReader, ownsServices(org) && !
|
|
352
|
+
const filesOrganisation = planFilesOrganisation(org, byKey, claimed, platformReadiness.filesReady, platformReadiness.missingKeys.files, platformReadiness.noKeyReader, ownsServices(org) && !recordedIds[`coolify:own-files:${org.id}`]);
|
|
354
353
|
if (filesOrganisation.step)
|
|
355
354
|
plannedSteps.push(filesOrganisation.step);
|
|
356
355
|
const aiOrganisation = planAiOrganisation(org, byKey, claimed, platformReadiness.aiReady, platformReadiness.missingKeys.ai, platformReadiness.noKeyReader);
|
|
@@ -441,7 +440,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
441
440
|
const edgeReservedLabels = planEdgeReservedLabels(org, recordedIds, platformReadiness, options.platformHostnames ?? []);
|
|
442
441
|
if (edgeReservedLabels.step)
|
|
443
442
|
plannedSteps.push(edgeReservedLabels.step);
|
|
444
|
-
const tunnel = planTunnel(org, byKey, claimed, recordedIds,
|
|
443
|
+
const tunnel = planTunnel(org, byKey, claimed, recordedIds, platformReadiness.platformApex);
|
|
445
444
|
// #238: a tunnel that is being provisioned waits for an active zone; parking
|
|
446
445
|
// one never does.
|
|
447
446
|
// #555: provisioning writes the tunnel config, the zone lookup and DNS; a
|
|
@@ -492,7 +491,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
492
491
|
const agoraGuarded = guardApp(agoraClient);
|
|
493
492
|
if (agoraGuarded)
|
|
494
493
|
plannedSteps.push(agoraGuarded);
|
|
495
|
-
const agora = planAgora(org, byKey, claimed, recordedIds, agoraHost,
|
|
494
|
+
const agora = planAgora(org, byKey, claimed, recordedIds, agoraHost, {
|
|
496
495
|
servesDiscovery: options.loginHostServesDiscovery ?? false,
|
|
497
496
|
});
|
|
498
497
|
if (agora.step)
|
|
@@ -521,7 +520,6 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
521
520
|
org,
|
|
522
521
|
recordedIds,
|
|
523
522
|
readiness: platformReadiness,
|
|
524
|
-
isPlatformOrg,
|
|
525
523
|
ownServices: options.ownServices ?? false,
|
|
526
524
|
tokenHeld: options.ownJobsTokenHeld ?? false,
|
|
527
525
|
});
|
|
@@ -572,6 +570,11 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
572
570
|
const recordedValues = new Set(Object.values(recordedIds));
|
|
573
571
|
for (const own of ['', org.id, org.slug, org.domain ?? ''])
|
|
574
572
|
recordedValues.delete(own);
|
|
573
|
+
// Archon's and the Access provider's project and client live in the owner company's ZITADEL.
|
|
574
|
+
if (platformReadiness.platformOwnerOrgId === org.id) {
|
|
575
|
+
for (const id of platformReadiness.platformZitadelIds ?? [])
|
|
576
|
+
recordedValues.add(id);
|
|
577
|
+
}
|
|
575
578
|
const platformNames = new Set([...(options.platformHostnames ?? []), platformReadiness.mailHost?.mailHost ?? ''].map((h) => h.toLowerCase()));
|
|
576
579
|
platformNames.delete('');
|
|
577
580
|
const unmanaged = live
|
|
@@ -691,11 +694,28 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
691
694
|
unmanaged.push(...app.blocked);
|
|
692
695
|
unmanaged.push(...brokerEntries.blocked);
|
|
693
696
|
unmanaged.push(...bouleBroker.blocked);
|
|
697
|
+
// F6: last, so every database step above runs before the backup of its database.
|
|
698
|
+
const backups = planCompanyBackups(org, recordedIds, options.backupStorageUuid);
|
|
699
|
+
plannedSteps.push(...backups.steps);
|
|
700
|
+
unmanaged.push(...backups.blocked);
|
|
701
|
+
// A company built on the shared network plans the move and nothing else: its other steps would act on
|
|
702
|
+
// resources the move deletes.
|
|
703
|
+
// The platform's own company stays on the platform network, so it is never moved.
|
|
704
|
+
const ownerCompany = onPlatformNetwork(isPlatformOwnOrg(org, platformReadiness.platformOwnerOrgId));
|
|
705
|
+
const networkMove = ownerCompany ? null : planNetworkMove(org, recordedIds);
|
|
706
|
+
if (!ownerCompany && networkMoveNeeded(recordedIds) && hasAdoptedResources(recordedIds)) {
|
|
707
|
+
unmanaged.push({
|
|
708
|
+
description: `${org.slug}'s adopted app and database stay on the shared Docker network: foundry did not create them and does not move them`,
|
|
709
|
+
key: 'coolify:network',
|
|
710
|
+
});
|
|
711
|
+
}
|
|
694
712
|
return {
|
|
695
713
|
orgId: org.id,
|
|
696
|
-
plannedSteps: (
|
|
697
|
-
?
|
|
698
|
-
:
|
|
714
|
+
plannedSteps: (networkMove
|
|
715
|
+
? [networkMove]
|
|
716
|
+
: ownSignInPending
|
|
717
|
+
? plannedSteps.filter((s) => !NEEDS_OWN_ZITADEL.has(`${s.action.system}:${s.action.kind}`))
|
|
718
|
+
: plannedSteps).map(withStepSystem),
|
|
699
719
|
unmanaged,
|
|
700
720
|
versionChanges,
|
|
701
721
|
orgPostgres,
|
|
@@ -787,7 +807,7 @@ function edgeBlockedItem(org, what, key, reason) {
|
|
|
787
807
|
* already exists live is unaffected: its own domain-move logic runs
|
|
788
808
|
* regardless, since that never re-provisions the ZITADEL org itself.
|
|
789
809
|
*/
|
|
790
|
-
function planIssuer(org, byKey, claimed, authReady
|
|
810
|
+
function planIssuer(org, byKey, claimed, authReady) {
|
|
791
811
|
const key = resourceKey('zitadel', 'issuer', org.id);
|
|
792
812
|
const live = byKey.get(key);
|
|
793
813
|
const desiredUsesDomain = org.issuer.usesDomain;
|
|
@@ -825,10 +845,8 @@ function planIssuer(org, byKey, claimed, authReady, isPlatformOrg = false) {
|
|
|
825
845
|
Boolean(org.domain) &&
|
|
826
846
|
liveHost !== null &&
|
|
827
847
|
liveHost !== loginHostnameFor(org);
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
if (isPlatformOrg ||
|
|
831
|
-
live.config.usesDomain === undefined ||
|
|
848
|
+
// A live issuer that does not say is not read as a different one either.
|
|
849
|
+
if (live.config.usesDomain === undefined ||
|
|
832
850
|
(Boolean(live.config.usesDomain) === desiredUsesDomain && !domainMoved)) {
|
|
833
851
|
return { step: null, blocked: null };
|
|
834
852
|
}
|
|
@@ -1011,27 +1029,10 @@ function planOwners(org, recordedIds, authReady, authIssuer, ownerInviteMail, re
|
|
|
1011
1029
|
});
|
|
1012
1030
|
return { steps, blocked };
|
|
1013
1031
|
}
|
|
1014
|
-
/**
|
|
1015
|
-
* #488: the bare hostname out of an issuer URL (`https://<hostname>`, the
|
|
1016
|
-
* only shape {@link agoraHostValuesFromPlatform} or a host-supplied
|
|
1017
|
-
* `AgoraHostPort` ever hands back), or `null` when there is no issuer to
|
|
1018
|
-
* read one from. What {@link loginHostnameFor} compares the org's own
|
|
1019
|
-
* computed `auth.<domain>` against, so the platform's own org never plans
|
|
1020
|
-
* its login host equal to the shared ZITADEL instance's own.
|
|
1021
|
-
*/
|
|
1032
|
+
/** True when `org` is the platform's owner company. */
|
|
1022
1033
|
export function isPlatformOwnOrg(org, platformOwnerOrgId) {
|
|
1023
1034
|
return Boolean(platformOwnerOrgId && org.id === platformOwnerOrgId);
|
|
1024
1035
|
}
|
|
1025
|
-
function platformAuthHostnameFrom(issuer) {
|
|
1026
|
-
if (!issuer)
|
|
1027
|
-
return null;
|
|
1028
|
-
try {
|
|
1029
|
-
return new URL(issuer).hostname;
|
|
1030
|
-
}
|
|
1031
|
-
catch {
|
|
1032
|
-
return null;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
1036
|
/**
|
|
1036
1037
|
* #267: the org's own company login host (`login-host.ts`'s
|
|
1037
1038
|
* `loginHostnameFor`) -- `auth.<domain>` once the org has one,
|
|
@@ -1068,54 +1069,24 @@ function platformAuthHostnameFrom(issuer) {
|
|
|
1068
1069
|
* platform's own domain, never plans the shared ZITADEL instance's own login
|
|
1069
1070
|
* host as its own -- `auth.companyincluded.com` colliding with itself.
|
|
1070
1071
|
*/
|
|
1071
|
-
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
1072
|
+
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired) {
|
|
1072
1073
|
const none = { step: null, blocked: null };
|
|
1073
1074
|
// An own org's instance has no shared login host to bind: its issuer is the sign-in service's own.
|
|
1074
|
-
if (ownsServices(org)
|
|
1075
|
+
if (ownsServices(org))
|
|
1075
1076
|
return none;
|
|
1076
1077
|
const key = resourceKey('zitadel', 'login-host', org.id);
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
// Before that host is known, plan nothing: a guessed host would delete the
|
|
1080
|
-
// working one, and a normal step would trust a domain `destroy` later deletes.
|
|
1081
|
-
if (isPlatformOrg && platformAuthHostname === null) {
|
|
1082
|
-
return {
|
|
1083
|
-
step: null,
|
|
1084
|
-
blocked: `${org.slug}'s login host is waiting for the platform auth host`,
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
const sharedInstanceHost = isPlatformOrg && platformAuthHostname !== null;
|
|
1088
|
-
const host = orgLoginHost(org, sharedInstanceHost ? platformAuthHostname : null);
|
|
1089
|
-
// #488: the guard in `loginHostnameFor` fired -- the org's own domain host
|
|
1090
|
-
// would have been the platform's shared ZITADEL instance host.
|
|
1091
|
-
if (sharedInstanceHost) {
|
|
1092
|
-
// The host is the shared instance's own: nothing to trust or bind, and an
|
|
1093
|
-
// org with no recorded host already signs in there. Only an org recorded
|
|
1094
|
-
// on another host (the old `<slug>-auth.oikos.host`) plans one step, to
|
|
1095
|
-
// move its apps over and remove the old host; then it plans nothing.
|
|
1096
|
-
const recordedHost = recordedIds[key];
|
|
1097
|
-
const moving = recordedHost !== undefined && recordedHost !== host;
|
|
1098
|
-
const removalOwed = pendingRemovalHosts(recordedIds).length > 0;
|
|
1099
|
-
if (!authReady || !(moving || removalOwed))
|
|
1100
|
-
return none;
|
|
1101
|
-
// The same gates as the normal branch: the executor's shared step also
|
|
1102
|
-
// needs the binding port and the ZITADEL org.
|
|
1103
|
-
const waiting = loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned);
|
|
1104
|
-
return waiting ?? stepFor(true);
|
|
1105
|
-
}
|
|
1106
|
-
function stepFor(shared) {
|
|
1078
|
+
const host = loginHostnameFor(org);
|
|
1079
|
+
function stepFor() {
|
|
1107
1080
|
return {
|
|
1108
1081
|
step: {
|
|
1109
1082
|
action: {
|
|
1110
1083
|
system: 'zitadel',
|
|
1111
1084
|
kind: 'login-host',
|
|
1112
1085
|
key: org.id,
|
|
1113
|
-
desired:
|
|
1086
|
+
desired: { host },
|
|
1114
1087
|
},
|
|
1115
1088
|
step: {
|
|
1116
|
-
description:
|
|
1117
|
-
? `move ${org.slug}'s apps onto the platform's own sign-in host ${host}`
|
|
1118
|
-
: `set ${org.slug}'s login host to ${host}`,
|
|
1089
|
+
description: `set ${org.slug}'s login host to ${host}`,
|
|
1119
1090
|
destroys: false,
|
|
1120
1091
|
migrates: false,
|
|
1121
1092
|
},
|
|
@@ -1143,7 +1114,7 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1143
1114
|
// Isolation between orgs rests on zitadel-login's binding, and repointing
|
|
1144
1115
|
// an app at a host with no binding breaks its sign-in: trust nothing and
|
|
1145
1116
|
// repoint nothing until the port that writes the binding is wired.
|
|
1146
|
-
return loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) ?? stepFor(
|
|
1117
|
+
return loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) ?? stepFor();
|
|
1147
1118
|
}
|
|
1148
1119
|
/**
|
|
1149
1120
|
* Every sign-in app this org already has live (web or native, not api) gets
|
|
@@ -1273,7 +1244,9 @@ export const JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY = 'jobs_registration_undeli
|
|
|
1273
1244
|
*/
|
|
1274
1245
|
function planMail(org, byKey, claimed, recordedIds, mailHost,
|
|
1275
1246
|
/** #491 (review round 2): read off `platformReadiness` directly, not off `mailHost` -- see `OrgPlatformReadiness.platformApex`'s own doc comment for why. */
|
|
1276
|
-
platformApex, platformLettermintProjectId
|
|
1247
|
+
platformApex, platformLettermintProjectId,
|
|
1248
|
+
/** Why the MX cannot be published yet: the relay does not route this org's mail domain. */
|
|
1249
|
+
relayBlocked) {
|
|
1277
1250
|
const key = resourceKey('coolify', 'mail', org.id);
|
|
1278
1251
|
const live = byKey.get(key);
|
|
1279
1252
|
// `off` means mail is not foundry's concern here (org.ts's MailMode doc);
|
|
@@ -1289,7 +1262,14 @@ platformApex, platformLettermintProjectId) {
|
|
|
1289
1262
|
if (!mailHost) {
|
|
1290
1263
|
return {
|
|
1291
1264
|
step: null,
|
|
1292
|
-
blocked: `${org.slug}'s mail is blocked, not planned:
|
|
1265
|
+
blocked: `${org.slug}'s mail is blocked, not planned: its own mail server is not applied yet`,
|
|
1266
|
+
mailboxBlocked: null,
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
if (relayBlocked) {
|
|
1270
|
+
return {
|
|
1271
|
+
step: null,
|
|
1272
|
+
blocked: `${org.slug}'s mail is blocked, not planned: ${relayBlocked}`,
|
|
1293
1273
|
mailboxBlocked: null,
|
|
1294
1274
|
};
|
|
1295
1275
|
}
|
|
@@ -1637,14 +1617,14 @@ function planChat(org, byKey, claimed, recordedIds, reading, issuerPlanned) {
|
|
|
1637
1617
|
* `sfu:livekit` step is declared. Parking, and an already-provisioned stack,
|
|
1638
1618
|
* are unaffected.
|
|
1639
1619
|
*/
|
|
1640
|
-
function planCalls(org, byKey, claimed, callsReady) {
|
|
1620
|
+
function planCalls(org, byKey, claimed, callsReady, synapseTag) {
|
|
1641
1621
|
const key = resourceKey('coolify', 'calls', org.id);
|
|
1642
1622
|
if (org.calls.enabled && !byKey.get(key) && !callsReady) {
|
|
1643
1623
|
return {
|
|
1644
1624
|
step: null,
|
|
1645
1625
|
blocked: ownsServices(org)
|
|
1646
1626
|
? `${org.slug}'s calls Matrix stack is blocked, not planned: its own media server (coolify:own-sfu) is not applied yet`
|
|
1647
|
-
: `${org.slug}'s calls Matrix stack is blocked, not planned: the platform's
|
|
1627
|
+
: `${org.slug}'s calls Matrix stack is blocked, not planned: the platform owner's calls SFU (the platform's) is off`,
|
|
1648
1628
|
};
|
|
1649
1629
|
}
|
|
1650
1630
|
// #248: the two images this stack's own apply would run, checked the same
|
|
@@ -1652,7 +1632,7 @@ function planCalls(org, byKey, claimed, callsReady) {
|
|
|
1652
1632
|
// own tag -- a re-apply of the same declaration must not silently run a
|
|
1653
1633
|
// different version with no diff shown anywhere.
|
|
1654
1634
|
if (org.calls.enabled && !byKey.get(key)) {
|
|
1655
|
-
const synapseBlocked = floatingTagBlocked('Synapse', VERSIONS.synapse, 'v1.161.0');
|
|
1635
|
+
const synapseBlocked = floatingTagBlocked('Synapse', synapseTag ?? VERSIONS.synapse, 'v1.161.0');
|
|
1656
1636
|
if (synapseBlocked) {
|
|
1657
1637
|
return {
|
|
1658
1638
|
step: null,
|
|
@@ -1667,10 +1647,12 @@ function planCalls(org, byKey, claimed, callsReady) {
|
|
|
1667
1647
|
};
|
|
1668
1648
|
}
|
|
1669
1649
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1650
|
+
const toggle = planHostingToggle('coolify', 'calls', org.calls.enabled, org, byKey, claimed, () => `provision ${org.slug}'s calls Matrix stack (Synapse + matrix-authentication-service)`, () => `park ${org.slug}'s calls Matrix stack (stops serving; data is retained, not deleted -- #7 corrections 1)`);
|
|
1651
|
+
// A held Synapse tag rides in the provisioning step; a parking step carries none.
|
|
1652
|
+
if (toggle && synapseTag && toggle.action.desired.enabled === true) {
|
|
1653
|
+
toggle.action.desired = { ...toggle.action.desired, synapseTag };
|
|
1654
|
+
}
|
|
1655
|
+
return { step: toggle, blocked: null };
|
|
1674
1656
|
}
|
|
1675
1657
|
/**
|
|
1676
1658
|
* The org's own calls service (#145, part 2 of #128) -- `wtfalch/calls`'s
|
|
@@ -2314,12 +2296,12 @@ function planAgoraClient(org, byKey, claimed) {
|
|
|
2314
2296
|
* message naming each missing value, which `computePlanDetail` lists with the
|
|
2315
2297
|
* plan's non-step items, so every other step still applies.
|
|
2316
2298
|
*/
|
|
2317
|
-
function planAgora(org, byKey, claimed, recordedIds, reading,
|
|
2299
|
+
function planAgora(org, byKey, claimed, recordedIds, reading, issuerSwitch) {
|
|
2318
2300
|
const key = resourceKey('coolify', 'agora', org.id);
|
|
2319
2301
|
const live = byKey.get(key);
|
|
2320
2302
|
if (live)
|
|
2321
2303
|
claimed.add(key);
|
|
2322
|
-
const ownSignIn = ownsServices(org)
|
|
2304
|
+
const ownSignIn = ownsServices(org);
|
|
2323
2305
|
if (ownSignIn && !ownAuthApplied(org, recordedIds)) {
|
|
2324
2306
|
return {
|
|
2325
2307
|
step: null,
|
|
@@ -2329,8 +2311,8 @@ function planAgora(org, byKey, claimed, recordedIds, reading, isPlatformOrg, iss
|
|
|
2329
2311
|
}
|
|
2330
2312
|
// The files host records its internal URL only while it applies, and Agora's
|
|
2331
2313
|
// env takes its files upstream from it: planned in the same pass, the first
|
|
2332
|
-
// deploy would start without one.
|
|
2333
|
-
if (ownSignIn &&
|
|
2314
|
+
// deploy would start without one.
|
|
2315
|
+
if (ownSignIn && !recordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY]) {
|
|
2334
2316
|
return {
|
|
2335
2317
|
step: null,
|
|
2336
2318
|
blocked: `${org.slug}'s Agora is blocked, not planned: its own files host is not applied yet`,
|
|
@@ -2360,7 +2342,7 @@ function planAgora(org, byKey, claimed, recordedIds, reading, isPlatformOrg, iss
|
|
|
2360
2342
|
// stays applied: a later failing read never flips it back (no oscillation).
|
|
2361
2343
|
let host = platformHost;
|
|
2362
2344
|
let issuerNote = null;
|
|
2363
|
-
const choice = agoraIssuerHostFor(org
|
|
2345
|
+
const choice = agoraIssuerHostFor(org);
|
|
2364
2346
|
if (choice.mismatch) {
|
|
2365
2347
|
issuerNote = `${org.slug}'s Agora cannot sign in: its login host ${loginHostnameFor(org)} is not on its Agora address ${agoraHostnameFor(org)}; the login host must be on the org's domain`;
|
|
2366
2348
|
}
|
|
@@ -2717,7 +2699,6 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2717
2699
|
const blockedKey = (rest) => entryKey === APP_ENTRY_KEY ? `coolify:app:${rest}` : `coolify:app:${entryKey}:${rest}`;
|
|
2718
2700
|
const { marker: wantedEnv, unresolved, unresolvedSecrets, } = entry
|
|
2719
2701
|
? resolveAppEnv(org, entry, recordedIds, {
|
|
2720
|
-
platformMailKey: readiness.mailKeyHeld === true,
|
|
2721
2702
|
sharedSecret: readiness.canHoldSecrets === true,
|
|
2722
2703
|
})
|
|
2723
2704
|
: { marker: {}, unresolved: [], unresolvedSecrets: [] };
|
|
@@ -2733,9 +2714,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2733
2714
|
.concat(
|
|
2734
2715
|
// Never a value: the ref of the missing key, or what the host lacks.
|
|
2735
2716
|
unresolvedSecrets.map(([name, source]) => ({
|
|
2736
|
-
description: `${org.slug}'s ${label} env ${name} is blocked, not planned: ${source
|
|
2737
|
-
? `the platform's Lettermint key is not held yet (held-key ref ${PLATFORM_AUTH_BROKER_MAIL_KEY_REF})`
|
|
2738
|
-
: `its source ${source} needs a held-key store that can put, and the host has none`}`,
|
|
2717
|
+
description: `${org.slug}'s ${label} env ${name} is blocked, not planned: ${`its source ${source} needs a held-key store that can put, and the host has none`}`,
|
|
2739
2718
|
key: blockedKey(`env:${name}`),
|
|
2740
2719
|
})))
|
|
2741
2720
|
: [];
|
|
@@ -2775,7 +2754,18 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2775
2754
|
key: blockedKey('repo'),
|
|
2776
2755
|
});
|
|
2777
2756
|
}
|
|
2778
|
-
|
|
2757
|
+
// A compose file can set `privileged`, host networking or a Docker socket mount, and its labels are
|
|
2758
|
+
// its own. `validateHosting` refuses the build pack on write; an entry stored before that rule is
|
|
2759
|
+
// held here too, and a live app of it is parked like one whose entry was removed.
|
|
2760
|
+
const composeBuilt = entry !== undefined && entry.buildPack === 'dockercompose';
|
|
2761
|
+
if (composeBuilt) {
|
|
2762
|
+
blocked.push({
|
|
2763
|
+
description: `${org.slug}'s ${label} is blocked, not planned: a compose-built app is refused until its compose file is checked`,
|
|
2764
|
+
key: blockedKey('compose'),
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
const refused = noRepo || composeBuilt;
|
|
2768
|
+
const toggled = planHostingToggle('coolify', 'app', entry !== undefined && !refused, org, byKey, claimed, () => `provision ${org.slug}'s ${label}`, () => `park ${org.slug}'s ${label} (stops serving; app data is retained, not deleted -- #7 corrections 1)`, stepKey);
|
|
2779
2769
|
// #447: only the live app's own `git_repository` decides, never the
|
|
2780
2770
|
// declared entry (a `repo: null` entry can sit over a bespoke app, which is
|
|
2781
2771
|
// parked as before). Only the `app` key ever held the default app, and an
|
|
@@ -2807,7 +2797,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2807
2797
|
}
|
|
2808
2798
|
if (toggled)
|
|
2809
2799
|
return { step: toggled, blocked };
|
|
2810
|
-
if (!entry ||
|
|
2800
|
+
if (!entry || refused)
|
|
2811
2801
|
return { step: null, blocked };
|
|
2812
2802
|
const live = byKey.get(resourceKey('coolify', 'app', stepKey));
|
|
2813
2803
|
if (!live)
|
|
@@ -2922,15 +2912,14 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2922
2912
|
blocked,
|
|
2923
2913
|
};
|
|
2924
2914
|
}
|
|
2925
|
-
//
|
|
2926
|
-
//
|
|
2927
|
-
if (
|
|
2928
|
-
JSON.stringify(appliedAppDomains(org, recordedIds, entryKey))) {
|
|
2915
|
+
// No entry carries Coolify `domains`: the company's tunnel routes its names straight to the container.
|
|
2916
|
+
// An app that still holds some (made before that) has them cleared and is redeployed.
|
|
2917
|
+
if (appliedAppDomains(org, recordedIds, entryKey).length > 0) {
|
|
2929
2918
|
return {
|
|
2930
2919
|
step: {
|
|
2931
2920
|
action,
|
|
2932
2921
|
step: {
|
|
2933
|
-
description: `
|
|
2922
|
+
description: `remove ${org.slug}'s ${label} Coolify domains (the tunnel routes it directly) and redeploy the app`,
|
|
2934
2923
|
destroys: false,
|
|
2935
2924
|
migrates: false,
|
|
2936
2925
|
},
|
|
@@ -2938,17 +2927,16 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2938
2927
|
blocked,
|
|
2939
2928
|
};
|
|
2940
2929
|
}
|
|
2941
|
-
// A
|
|
2942
|
-
//
|
|
2943
|
-
//
|
|
2930
|
+
// A changed hostname, wildcard or port, or a container still open to Traefik: the label set
|
|
2931
|
+
// (`traefik.enable=false`) is patched and the app redeployed once the marker `coolifyApp`
|
|
2932
|
+
// recorded differs from the wanted one.
|
|
2944
2933
|
const appliedLabels = recordedIds[appLabelsAppliedScopeKey(entryKey)];
|
|
2945
|
-
|
|
2946
|
-
if (labelSpec && entryLabelHash(labelSpec) !== appliedLabels) {
|
|
2934
|
+
if (entryLabelHash(entryLabelSpec(org, entry)) !== appliedLabels) {
|
|
2947
2935
|
return {
|
|
2948
2936
|
step: {
|
|
2949
2937
|
action,
|
|
2950
2938
|
step: {
|
|
2951
|
-
description: `apply ${org.slug}'s ${label}
|
|
2939
|
+
description: `apply ${org.slug}'s ${label} routing (hostnames, port, no Traefik route) and redeploy the app`,
|
|
2952
2940
|
destroys: false,
|
|
2953
2941
|
migrates: false,
|
|
2954
2942
|
},
|
|
@@ -3014,12 +3002,11 @@ function planApps(org, byKey, claimed, recordedIds, plannedSteps, authIssuer, re
|
|
|
3014
3002
|
// The same availability the plan's marker used: the executor records its
|
|
3015
3003
|
// marker from this, never from its own port, so the two cannot disagree.
|
|
3016
3004
|
const usesSecrets = org.hosting.entries.some((e) => e.key === key &&
|
|
3017
|
-
Object.values(e.env).some((v) => typeof v !== 'string' &&
|
|
3005
|
+
Object.values(e.env).some((v) => typeof v !== 'string' && v.from === 'sharedSecret'));
|
|
3018
3006
|
if (usesSecrets && app.step && app.step.action.desired.enabled !== false) {
|
|
3019
3007
|
app.step.action.desired = {
|
|
3020
3008
|
...app.step.action.desired,
|
|
3021
3009
|
secretSources: {
|
|
3022
|
-
platformMailKey: readiness.mailKeyHeld === true,
|
|
3023
3010
|
sharedSecret: readiness.canHoldSecrets === true,
|
|
3024
3011
|
},
|
|
3025
3012
|
};
|
|
@@ -3184,11 +3171,13 @@ function sortedNames(names) {
|
|
|
3184
3171
|
* domain) plans no per-org tunnel for Agora: `<slug>-agora.oikos.host` rides
|
|
3185
3172
|
* the shared `*.oikos.host` wildcard tunnel (`scripts/shared-ingress.mjs`).
|
|
3186
3173
|
*/
|
|
3187
|
-
function planTunnel(org, byKey, claimed, recordedIds,
|
|
3174
|
+
function planTunnel(org, byKey, claimed, recordedIds, platformApex) {
|
|
3188
3175
|
const desired = (org.hosting.tunnel && org.hosting.usesDomain && Boolean(org.domain)) ||
|
|
3189
3176
|
(org.site.enabled && Boolean(org.domain)) ||
|
|
3190
3177
|
(org.calls.enabled && org.calls.usesDomain && Boolean(org.domain)) ||
|
|
3191
|
-
Boolean(org.domain)
|
|
3178
|
+
Boolean(org.domain) ||
|
|
3179
|
+
// A company without a domain still gets a tunnel of its own for an entry at `<slug>.oikos.host`.
|
|
3180
|
+
entryRoutes(org).length > 0;
|
|
3192
3181
|
const toggled = planHostingToggle('cloudflare', 'tunnel', desired, org, byKey, claimed, () => `provision a Cloudflare tunnel for ${org.slug}'s app`, () => `park ${org.slug}'s Cloudflare tunnel (configuration is retained, not deleted -- #7 corrections 1)`, org.id,
|
|
3193
3182
|
// Cloudflare's status, read against what is declared: a park cannot change
|
|
3194
3183
|
// it, so judging on/off by status alone would never let the plan empty.
|
|
@@ -3204,7 +3193,7 @@ function planTunnel(org, byKey, claimed, recordedIds, isPlatformOrg, platformApe
|
|
|
3204
3193
|
// #568: the company's own login host is routed by the step, so a first
|
|
3205
3194
|
// provision carries it too; a park does not. Without a known platform apex
|
|
3206
3195
|
// the platform's own org cannot be told apart, so none is planned.
|
|
3207
|
-
const loginHost = platformApex ? companyLoginTunnelHost(org
|
|
3196
|
+
const loginHost = platformApex ? companyLoginTunnelHost(org) : null;
|
|
3208
3197
|
if (toggled && toggled.action.desired.enabled !== false && loginHost) {
|
|
3209
3198
|
return {
|
|
3210
3199
|
...toggled,
|
|
@@ -3236,8 +3225,17 @@ function planTunnel(org, byKey, claimed, recordedIds, isPlatformOrg, platformApe
|
|
|
3236
3225
|
org.domain !== null &&
|
|
3237
3226
|
sortedNames(published.split(',')) !==
|
|
3238
3227
|
sortedNames(tunnelDnsHostnames(org, org.domain, loginHost));
|
|
3239
|
-
|
|
3240
|
-
|
|
3228
|
+
// The entry names in the platform's oikos.host zone are recorded apart from the company's own zone.
|
|
3229
|
+
const oikosStale = (recordedIds[CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY] ?? '') !==
|
|
3230
|
+
entryOikosHostnames(org).join(',');
|
|
3231
|
+
const dnsStale = (org.domain !== null && recordedIds[CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY] !== org.domain) ||
|
|
3232
|
+
edgeChanged ||
|
|
3233
|
+
oikosStale;
|
|
3234
|
+
// An entry's hostname goes straight to its container, so a changed port, key or name, or an ingress
|
|
3235
|
+
// written before entries were routed that way, needs the ingress written again. Nothing recorded
|
|
3236
|
+
// equals no routes.
|
|
3237
|
+
const routesStale = (recordedIds[CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY] ?? '[]') !== entryRoutesMarker(org);
|
|
3238
|
+
if (!connectorMissing && !dnsStale && !routesStale)
|
|
3241
3239
|
return null;
|
|
3242
3240
|
return {
|
|
3243
3241
|
action: {
|
|
@@ -3249,7 +3247,9 @@ function planTunnel(org, byKey, claimed, recordedIds, isPlatformOrg, platformApe
|
|
|
3249
3247
|
step: {
|
|
3250
3248
|
description: connectorMissing
|
|
3251
3249
|
? `run a cloudflared connector for ${org.slug}'s Cloudflare tunnel`
|
|
3252
|
-
:
|
|
3250
|
+
: dnsStale
|
|
3251
|
+
? `point ${org.slug}'s hostnames at its Cloudflare tunnel`
|
|
3252
|
+
: `route ${org.slug}'s entry hostnames straight to their containers`,
|
|
3253
3253
|
destroys: false,
|
|
3254
3254
|
migrates: false,
|
|
3255
3255
|
},
|
|
@@ -10,6 +10,10 @@ export interface AccessAppSummary {
|
|
|
10
10
|
id: string;
|
|
11
11
|
name: string;
|
|
12
12
|
domain: string;
|
|
13
|
+
/** The identity providers the app accepts; `[]` means Cloudflare lists none (any provider). */
|
|
14
|
+
allowedIdps: string[];
|
|
15
|
+
/** Whether a visitor goes straight to the one provider. */
|
|
16
|
+
autoRedirectToIdentity: boolean;
|
|
13
17
|
}
|
|
14
18
|
/**
|
|
15
19
|
* Foundry's Cloudflare Access calls. Every Access call in foundry goes through
|
|
@@ -19,13 +23,24 @@ export interface AccessAppSummary {
|
|
|
19
23
|
export interface AccessPort {
|
|
20
24
|
/** Every live Access app foundry may use: the platform's own, and ones nobody adopted yet. */
|
|
21
25
|
listAccessApps(): Promise<AccessAppSummary[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Edge keeps `allowedIdps` of an app that already exists: a repeat create
|
|
28
|
+
* only reports the live value, so a caller compares it from
|
|
29
|
+
* {@link listAccessApps} and rebuilds the app on a difference.
|
|
30
|
+
*/
|
|
22
31
|
createAccessApp(app: {
|
|
23
32
|
name: string;
|
|
24
33
|
domain: string;
|
|
25
34
|
type: 'self_hosted';
|
|
35
|
+
allowedIdps?: string[];
|
|
36
|
+
autoRedirectToIdentity?: boolean;
|
|
26
37
|
}): Promise<{
|
|
27
38
|
id: string;
|
|
28
39
|
}>;
|
|
40
|
+
/** The account's one-time-PIN identity provider (edge 0.13.0 finds or makes it). Safe to repeat. */
|
|
41
|
+
ensureOneTimePinProvider(): Promise<{
|
|
42
|
+
id: string;
|
|
43
|
+
}>;
|
|
29
44
|
listAccessPolicies(appId: string): Promise<Array<{
|
|
30
45
|
id: string;
|
|
31
46
|
name: string;
|