@wtfalch/foundry 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calls-sfu.d.ts +6 -0
- package/dist/calls-sfu.js +17 -8
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/migrations/0050_platform_sfu_connection_revision.sql +7 -0
- package/dist/plan/boule.d.ts +27 -3
- package/dist/plan/boule.js +39 -1
- package/dist/plan/diff.js +49 -6
- package/dist/plan/executor.d.ts +46 -7
- package/dist/plan/executor.js +169 -26
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +2 -2
- package/dist/plan/live.d.ts +3 -3
- package/dist/plan/plan-apply.js +4 -0
- package/dist/plan/platform-diff.d.ts +10 -0
- package/dist/plan/platform-diff.js +29 -14
- package/dist/plan/platform-org.d.ts +3 -4
- package/dist/plan/platform-providers.d.ts +1 -1
- package/dist/plan/platform-providers.js +16 -9
- package/dist/plan/platform-sfu.d.ts +10 -0
- package/dist/plan/platform-sfu.js +39 -0
- package/dist/plan/providers-readers.d.ts +1 -1
- package/dist/plan/providers-readers.js +19 -4
- package/dist/platform.d.ts +11 -1
- package/dist/platform.js +6 -0
- package/dist/store/postgres.js +7 -1
- package/dist/store/schema.d.ts +17 -0
- package/dist/store/schema.js +2 -0
- package/package.json +1 -1
package/dist/plan/executor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { URL } from 'node:url';
|
|
|
3
3
|
import { resolveCallsSfuUrls } from '../calls-sfu.js';
|
|
4
4
|
import { APP_ENTRY_KEY, appEntry, filesCollectionEnvName, ownerMailboxRecorded, ownerMailboxes, sharedSecretRef, } from '../org.js';
|
|
5
5
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatIntrospectionAppName, desiredAgoraEnv, entryClientIdScopeKey, } from './agora.js';
|
|
6
|
-
import { BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_DATABASE_SCOPE_KEY, BOULE_DEPLOYED_SCOPE_KEY, bouleEnvDrift, bouleHostnameFor, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
6
|
+
import { BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_DATABASE_SCOPE_KEY, BOULE_DEPLOYED_SCOPE_KEY, bouleEnvDrift, bouleHostnameFor, bouleMailWanted, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
7
7
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, writeBrokerEntry, } from './broker-entries.js';
|
|
8
8
|
import { MAS_IMAGE, MAS_TAG, SYNAPSE_IMAGE, SYNAPSE_TAG, callsMasConfigYaml, callsServerName, callsSynapseHomeserverYaml, mintCallsMatrixSecrets, parseCallsDbUrl, } from './calls-matrix.js';
|
|
9
9
|
import { CHAT_REPO, CHAT_SERVICE_DOCKERFILE, CHAT_SERVICE_PORT, CHAT_SYNAPSE_DOCKERFILE, CHAT_SYNAPSE_PORT, chatInternalNames, chatMasConfigYaml, chatServiceEnvs, chatSynapseHomeserverYaml, chatUpstreams, } from './chat-matrix.js';
|
|
@@ -354,10 +354,10 @@ const CMS_DATABASE_SCOPE_KEY = 'coolify_cms_database';
|
|
|
354
354
|
* domain choice, the same way {@link APP_CLIENT_ID_SCOPE_KEY} carries a
|
|
355
355
|
* public value out. All of that org's Lettermint work -- ensure domain,
|
|
356
356
|
* restrict, publish DNS, verify -- now happens inside the same step that
|
|
357
|
-
* reads this key (`ensureAppLettermintMail`), so unlike the
|
|
357
|
+
* reads this key (`ensureAppLettermintMail`), so unlike the earlier design
|
|
358
358
|
* this rework replaces, no separate domain-id scope key is recorded: the
|
|
359
359
|
* domain is looked up by name fresh each apply. Renamed from
|
|
360
|
-
*
|
|
360
|
+
* the earlier mail domain key.
|
|
361
361
|
*/
|
|
362
362
|
const MAIL_DOMAIN_SCOPE_KEY = 'mail_domain';
|
|
363
363
|
/**
|
|
@@ -2493,9 +2493,13 @@ export class FoundryStepExecutor {
|
|
|
2493
2493
|
* domain restricted to them before any old project is deleted, so the
|
|
2494
2494
|
* restriction list never becomes empty.
|
|
2495
2495
|
*
|
|
2496
|
+
* #654: the non-platform-apex part now lives in {@link ensureOrgAppMailToken},
|
|
2497
|
+
* shared with Boule, which holds the same token; the rule below is its rule
|
|
2498
|
+
* with the consumers read from the hosted app and Boule.
|
|
2499
|
+
*
|
|
2496
2500
|
* Mint rule ("Credential model"): the project is healthy when the app env
|
|
2497
2501
|
* holds `LETTERMINT_API_TOKEN` and Lettermint lists exactly one project
|
|
2498
|
-
* named `<slug>-app` -- unlike
|
|
2502
|
+
* named `<slug>-app` -- unlike the earlier provider's key, a Lettermint project's name
|
|
2499
2503
|
* carries no domain id, since a domain switch is handled entirely by
|
|
2500
2504
|
* {@link LettermintPort.restrictDomainToProjects} below and never needs a
|
|
2501
2505
|
* fresh project. Otherwise a fresh project is created and its token
|
|
@@ -2545,7 +2549,6 @@ export class FoundryStepExecutor {
|
|
|
2545
2549
|
if (!hasPlatformApexField) {
|
|
2546
2550
|
throw new Error(`coolify:app for ${org.slug}: this step's plan carries no platformApex; plan this org's app again`);
|
|
2547
2551
|
}
|
|
2548
|
-
const appName = `${org.slug}-app`;
|
|
2549
2552
|
// #491: an org whose mail domain is the platform's own apex (Company
|
|
2550
2553
|
// Included on companyincluded.com, today) shares the platform's existing
|
|
2551
2554
|
// Lettermint project rather than minting a `<slug>-app` one of its own --
|
|
@@ -2588,44 +2591,129 @@ export class FoundryStepExecutor {
|
|
|
2588
2591
|
await this.ports.coolify.restart(appId);
|
|
2589
2592
|
return;
|
|
2590
2593
|
}
|
|
2591
|
-
|
|
2594
|
+
await this.ensureOrgAppMailToken({
|
|
2595
|
+
org,
|
|
2596
|
+
context,
|
|
2597
|
+
mailDomain,
|
|
2598
|
+
self: { role: 'app', id: appId, restart: running },
|
|
2599
|
+
mailFrom: `${org.mail.noReplyLocalPart ?? 'no-reply'}@${mailDomain}`,
|
|
2600
|
+
platformApex,
|
|
2601
|
+
platformLettermintProjectId,
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* The one mint rule for the org's `<slug>-app` Lettermint token, shared by
|
|
2606
|
+
* the hosted app (`ensureAppLettermintMail`) and Boule
|
|
2607
|
+
* (`ensureBouleMail`), which hold the same token. Lettermint shows a
|
|
2608
|
+
* token once, so the token lives only in those apps' envs: the rule reads
|
|
2609
|
+
* every consumer that is recorded (the hosted app first, Boule second).
|
|
2610
|
+
* The project is healthy when Lettermint lists exactly one `<slug>-app`
|
|
2611
|
+
* project and some consumer holds a token. A healthy rule gives `self` that
|
|
2612
|
+
* token (the app's wins) when it lacks it or holds another; a peer gets it
|
|
2613
|
+
* in its own step. An
|
|
2614
|
+
* unhealthy one mints a fresh project and writes its token to every
|
|
2615
|
+
* consumer, restarting those that run, before any other `<slug>-app`
|
|
2616
|
+
* project is deleted. Either role may run first: an app added after Boule
|
|
2617
|
+
* adopts Boule's token instead of deleting its project.
|
|
2618
|
+
*
|
|
2619
|
+
* The token goes straight to `bulkUpsertEnvs` and nowhere else: never
|
|
2620
|
+
* returned, recorded, logged or interpolated into an error. Returns
|
|
2621
|
+
* whether `self`'s own token changed and whether it was restarted here.
|
|
2622
|
+
*
|
|
2623
|
+
* A peer recorded but gone from Coolify (404) is no consumer. A healthy
|
|
2624
|
+
* company's step writes only its own env; a re-mint writes every live
|
|
2625
|
+
* consumer, restarts the running ones, restricts the domain, then deletes
|
|
2626
|
+
* the old projects.
|
|
2627
|
+
*/
|
|
2628
|
+
async ensureOrgAppMailToken(args) {
|
|
2629
|
+
const { org, context, mailDomain, self, mailFrom } = args;
|
|
2630
|
+
const appName = `${org.slug}-app`;
|
|
2631
|
+
const recorded = context.recordedIds();
|
|
2632
|
+
const appId = self.role === 'app' ? self.id : recorded[`coolify:app:${appStepKey(org)}`];
|
|
2633
|
+
const bouleId = self.role === 'boule' ? self.id : recorded[`coolify:boule:${org.id}`];
|
|
2634
|
+
const peerListEnvs = (id) => !id || id === self.id
|
|
2635
|
+
? Promise.resolve(null)
|
|
2636
|
+
: // A recorded peer that Coolify no longer has is not a consumer: not
|
|
2637
|
+
// read, written or restarted (same 404 rule as the retire step).
|
|
2638
|
+
this.ports.coolify
|
|
2639
|
+
.listEnvs(id)
|
|
2640
|
+
.catch((error) => {
|
|
2641
|
+
if (!isNotFoundError(error))
|
|
2642
|
+
throw error;
|
|
2643
|
+
return null;
|
|
2644
|
+
});
|
|
2645
|
+
const [projects, selfEnvs, appEnvs, bouleEnvs] = await Promise.all([
|
|
2592
2646
|
this.ports.lettermint.listProjects(),
|
|
2593
|
-
this.ports.coolify.listEnvs(
|
|
2647
|
+
this.ports.coolify.listEnvs(self.id),
|
|
2648
|
+
self.role === 'app' ? Promise.resolve(null) : peerListEnvs(appId),
|
|
2649
|
+
self.role === 'boule' ? Promise.resolve(null) : peerListEnvs(bouleId),
|
|
2594
2650
|
]);
|
|
2595
|
-
const
|
|
2651
|
+
const tokenOf = (envs) => envs.find((e) => e.key === 'LETTERMINT_API_TOKEN' && !e.is_preview)?.value || undefined;
|
|
2652
|
+
// The hosted app first, Boule second: the app's token wins a disagreement.
|
|
2653
|
+
const consumers = [];
|
|
2654
|
+
const appSlot = self.role === 'app' ? selfEnvs : appEnvs;
|
|
2655
|
+
const bouleSlot = self.role === 'boule' ? selfEnvs : bouleEnvs;
|
|
2656
|
+
if (appSlot && appId)
|
|
2657
|
+
consumers.push({ id: appId, token: tokenOf(appSlot) });
|
|
2658
|
+
if (bouleSlot && bouleId)
|
|
2659
|
+
consumers.push({ id: bouleId, token: tokenOf(bouleSlot) });
|
|
2660
|
+
const existingToken = consumers.find((c) => c.token)?.token;
|
|
2596
2661
|
const ours = projects.filter((p) => p.name === appName);
|
|
2597
2662
|
const held = ours.length === 1 ? ours[0] : undefined;
|
|
2598
|
-
const healthy = held !== undefined &&
|
|
2663
|
+
const healthy = held !== undefined && existingToken !== undefined;
|
|
2599
2664
|
let allProjects = projects;
|
|
2600
2665
|
let keepProjectId;
|
|
2601
|
-
|
|
2602
|
-
if (healthy && held) {
|
|
2666
|
+
let token;
|
|
2667
|
+
if (healthy && held && existingToken) {
|
|
2603
2668
|
keepProjectId = held.id;
|
|
2669
|
+
token = existingToken;
|
|
2604
2670
|
}
|
|
2605
2671
|
else {
|
|
2606
2672
|
const created = await this.ports.lettermint.createProject(appName);
|
|
2607
2673
|
keepProjectId = created.project.id;
|
|
2608
2674
|
allProjects = [...projects, created.project];
|
|
2609
|
-
|
|
2675
|
+
token = created.token;
|
|
2676
|
+
}
|
|
2677
|
+
// A healthy company: this step writes only its own env; a peer that lacks
|
|
2678
|
+
// the token gets it in its own step. A re-mint kills the old token, so it
|
|
2679
|
+
// reaches every live consumer here.
|
|
2680
|
+
const targets = consumers.filter((c) => c.token !== token && (!healthy || c.id === self.id));
|
|
2681
|
+
const selfChanged = targets.some((c) => c.id === self.id);
|
|
2682
|
+
for (const consumer of consumers) {
|
|
2683
|
+
const updates = [];
|
|
2684
|
+
if (targets.includes(consumer))
|
|
2685
|
+
updates.push({ key: 'LETTERMINT_API_TOKEN', value: token });
|
|
2686
|
+
if (consumer.id === self.id && mailFrom)
|
|
2687
|
+
updates.push({ key: 'MAIL_FROM', value: mailFrom });
|
|
2688
|
+
if (updates.length > 0)
|
|
2689
|
+
await this.ports.coolify.bulkUpsertEnvs(consumer.id, updates);
|
|
2610
2690
|
}
|
|
2611
|
-
envUpdates.push({
|
|
2612
|
-
key: 'MAIL_FROM',
|
|
2613
|
-
value: `${org.mail.noReplyLocalPart ?? 'no-reply'}@${mailDomain}`,
|
|
2614
|
-
});
|
|
2615
|
-
await this.ports.coolify.bulkUpsertEnvs(appId, envUpdates);
|
|
2616
2691
|
// A running container read its env at start and still holds the old
|
|
2617
|
-
// token, so a fresh one only reaches it through a restart --
|
|
2618
|
-
// old project is deleted below
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2692
|
+
// token, so a fresh one only reaches it through a restart -- after every
|
|
2693
|
+
// write and before the old project is deleted below, so a failure in
|
|
2694
|
+
// between leaves the old project for the next apply to re-mint. A just-created
|
|
2695
|
+
// app is deployed first (#566).
|
|
2696
|
+
let selfRestarted = false;
|
|
2697
|
+
for (const consumer of targets) {
|
|
2698
|
+
const isSelf = consumer.id === self.id;
|
|
2699
|
+
const running = isSelf
|
|
2700
|
+
? self.restart
|
|
2701
|
+
: isRunningOrStarting((await this.ports.coolify.getApplication(consumer.id)).status);
|
|
2702
|
+
if (!running)
|
|
2703
|
+
continue;
|
|
2704
|
+
await this.ports.coolify.restart(consumer.id);
|
|
2705
|
+
if (isSelf)
|
|
2706
|
+
selfRestarted = true;
|
|
2707
|
+
}
|
|
2708
|
+
await this.ensureLettermintDomainRestriction(org, mailDomain, allProjects, args.platformApex, args.platformLettermintProjectId);
|
|
2622
2709
|
for (const project of allProjects) {
|
|
2623
2710
|
if (project.name === appName &&
|
|
2624
2711
|
project.id !== keepProjectId &&
|
|
2625
|
-
project.id !== platformLettermintProjectId) {
|
|
2712
|
+
project.id !== args.platformLettermintProjectId) {
|
|
2626
2713
|
await this.ports.lettermint.deleteProject(project.id);
|
|
2627
2714
|
}
|
|
2628
2715
|
}
|
|
2716
|
+
return { selfChanged, selfRestarted };
|
|
2629
2717
|
}
|
|
2630
2718
|
/**
|
|
2631
2719
|
* #149: mints this org's own Lettermint relay credential and writes it to
|
|
@@ -4237,7 +4325,14 @@ export class FoundryStepExecutor {
|
|
|
4237
4325
|
// written is the env the plan showed.
|
|
4238
4326
|
const issuer = action.desired.issuer ?? null;
|
|
4239
4327
|
const archonUrl = action.desired.archonUrl ?? null;
|
|
4240
|
-
|
|
4328
|
+
// The plan decides blocked mail (`mailBlocked`); a plan with no
|
|
4329
|
+
// `platformApex` key predates this field and is refused before any mail
|
|
4330
|
+
// env is written.
|
|
4331
|
+
const mailWanted = bouleMailWanted(org, context.recordedIds()) && action.desired.mailBlocked !== true;
|
|
4332
|
+
if (mailWanted && !('platformApex' in action.desired)) {
|
|
4333
|
+
throw new Error(`coolify:boule for ${org.slug}: this step's plan carries no platformApex; plan this org's Boule again`);
|
|
4334
|
+
}
|
|
4335
|
+
const { env, missing } = desiredBouleEnv(org, context.recordedIds(), issuer, archonUrl, mailWanted);
|
|
4241
4336
|
if (missing.length > 0) {
|
|
4242
4337
|
throw new Error(`Boule for ${org.slug} cannot be configured: missing ${missing.join(', ')}. Nothing was written.`);
|
|
4243
4338
|
}
|
|
@@ -4288,6 +4383,12 @@ export class FoundryStepExecutor {
|
|
|
4288
4383
|
if (!dbPush.pushed) {
|
|
4289
4384
|
throw new Error(`Boule for ${org.slug}: its database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
4290
4385
|
}
|
|
4386
|
+
// The org's Lettermint token, the same one its hosted app holds. A no-op
|
|
4387
|
+
// for an org with mail off or no mail domain recorded: Boule then shows
|
|
4388
|
+
// the invite link.
|
|
4389
|
+
const mail = mailWanted
|
|
4390
|
+
? await this.ensureBouleMail(app.appId, org, context, action, app.existed && Boolean(context.recordedIds()[BOULE_DEPLOYED_SCOPE_KEY]))
|
|
4391
|
+
: { changed: false, restarted: false };
|
|
4291
4392
|
// #504: see `coolifyAgora`'s own comment just above -- `createGithubAppApplication`
|
|
4292
4393
|
// never sets `instant_deploy`, so a brand new app (or one found by name
|
|
4293
4394
|
// that a previous apply created but crashed before deploying) sat built
|
|
@@ -4300,11 +4401,53 @@ export class FoundryStepExecutor {
|
|
|
4300
4401
|
context.recordScopeId(BOULE_DEPLOYED_SCOPE_KEY, '1');
|
|
4301
4402
|
}
|
|
4302
4403
|
else if (app.existed &&
|
|
4303
|
-
|
|
4404
|
+
!mail.restarted &&
|
|
4405
|
+
(envChanged || cookieSecret !== null || appKey !== null || dbPush.changed || mail.changed)) {
|
|
4406
|
+
// A restart inside the mail rule already came after every env write above.
|
|
4304
4407
|
await this.ports.coolify.restart(app.appId);
|
|
4305
4408
|
}
|
|
4306
4409
|
return { resourceId: app.appId };
|
|
4307
4410
|
}
|
|
4411
|
+
/**
|
|
4412
|
+
* Gives Boule `LETTERMINT_API_TOKEN`: the platform's held key for an org on
|
|
4413
|
+
* the platform's own apex, else the org's `<slug>-app` token through the
|
|
4414
|
+
* shared mint rule ({@link ensureOrgAppMailToken}), which mints that project
|
|
4415
|
+
* when no hosted app made it yet. `MAIL_FROM` is written with the rest of
|
|
4416
|
+
* Boule's env. Returns whether Boule's token changed, so the caller
|
|
4417
|
+
* restarts a running Boule, unless `restarted`: the shared rule then already
|
|
4418
|
+
* restarted it (`running`), before it deleted the old project.
|
|
4419
|
+
*/
|
|
4420
|
+
async ensureBouleMail(appId, org, context, action, running) {
|
|
4421
|
+
const none = { changed: false, restarted: false };
|
|
4422
|
+
const mailDomain = context.recordedIds()[MAIL_DOMAIN_SCOPE_KEY];
|
|
4423
|
+
if (!mailDomain)
|
|
4424
|
+
return none;
|
|
4425
|
+
const platformApex = action.desired.platformApex ?? null;
|
|
4426
|
+
const platformLettermintProjectId = action.desired.platformLettermintProjectId ?? null;
|
|
4427
|
+
if (platformApex && mailDomain.toLowerCase() === platformApex.toLowerCase()) {
|
|
4428
|
+
const heldToken = (await this.ports.heldKeys?.resolve(PLATFORM_AUTH_BROKER_MAIL_KEY_REF)) ?? null;
|
|
4429
|
+
if (!heldToken) {
|
|
4430
|
+
throw new Error(`coolify:boule for ${org.slug}: the platform's own Lettermint token is not held yet (held-key ref ${PLATFORM_AUTH_BROKER_MAIL_KEY_REF})`);
|
|
4431
|
+
}
|
|
4432
|
+
const current = (await this.ports.coolify.listEnvs(appId)).find((e) => e.key === 'LETTERMINT_API_TOKEN' && !e.is_preview)?.value;
|
|
4433
|
+
if (current === heldToken)
|
|
4434
|
+
return none;
|
|
4435
|
+
await this.ports.coolify.bulkUpsertEnvs(appId, [
|
|
4436
|
+
{ key: 'LETTERMINT_API_TOKEN', value: heldToken },
|
|
4437
|
+
]);
|
|
4438
|
+
return { changed: true, restarted: false };
|
|
4439
|
+
}
|
|
4440
|
+
const { selfChanged, selfRestarted } = await this.ensureOrgAppMailToken({
|
|
4441
|
+
org,
|
|
4442
|
+
context,
|
|
4443
|
+
mailDomain,
|
|
4444
|
+
self: { role: 'boule', id: appId, restart: running },
|
|
4445
|
+
mailFrom: null,
|
|
4446
|
+
platformApex,
|
|
4447
|
+
platformLettermintProjectId,
|
|
4448
|
+
});
|
|
4449
|
+
return { changed: selfChanged, restarted: selfRestarted };
|
|
4450
|
+
}
|
|
4308
4451
|
// ---- jobs (#173) ---------------------------------------------------------
|
|
4309
4452
|
/**
|
|
4310
4453
|
* `plan/diff.ts`'s `planJobsRegistration`: registers the org's own app with
|
|
@@ -5188,7 +5331,7 @@ export class FoundryStepExecutor {
|
|
|
5188
5331
|
* upsert `publishMailDnsRecords` above uses for Stalwart's own records --
|
|
5189
5332
|
* except an existing `_dmarc` record is never overwritten (a stricter
|
|
5190
5333
|
* policy an org already set, `p=quarantine`/`p=reject`, should not be
|
|
5191
|
-
* relaxed back to Lettermint's own default).
|
|
5334
|
+
* relaxed back to Lettermint's own default). The earlier provider's domain also
|
|
5192
5335
|
* carried an apex `SPF` TXT record that had to be merged into whatever SPF
|
|
5193
5336
|
* record already existed there (RFC 7208: two `v=spf1` TXT records on one
|
|
5194
5337
|
* name is a permerror); Lettermint's own DNS records
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { computePlanDetail, toPlan } from './diff.js';
|
|
|
5
5
|
export type { StepSystem } from './step-system.js';
|
|
6
6
|
export { stepSystemOf } from './step-system.js';
|
|
7
7
|
export type { PlatformPlanDetail } from './platform-diff.js';
|
|
8
|
-
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, } from './platform-diff.js';
|
|
8
|
+
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
9
9
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
|
|
10
10
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
11
11
|
export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
|
@@ -23,7 +23,7 @@ export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from '
|
|
|
23
23
|
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
24
24
|
export { createZitadelProjectRoles } from './zitadel-project-roles.js';
|
|
25
25
|
export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './platform-sfu.js';
|
|
26
|
-
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
26
|
+
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
27
27
|
export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './mail-copy.js';
|
|
28
28
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
29
29
|
export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './providers-readers.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
2
2
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
3
3
|
export { stepSystemOf } from './step-system.js';
|
|
4
|
-
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, } from './platform-diff.js';
|
|
4
|
+
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
5
5
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
6
6
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
7
7
|
export { AdoptRefusedError } from './adopt.js';
|
|
@@ -12,7 +12,7 @@ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEME
|
|
|
12
12
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
13
13
|
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
14
14
|
export { createZitadelProjectRoles } from './zitadel-project-roles.js';
|
|
15
|
-
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
15
|
+
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
16
16
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
17
17
|
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
18
18
|
export { PlatformBrokerPort } from './broker-entries.js';
|
package/dist/plan/live.d.ts
CHANGED
|
@@ -23,9 +23,9 @@ export interface LiveResource {
|
|
|
23
23
|
/**
|
|
24
24
|
* `'sfu'` (#170) has no reader wired below -- there is no cheap way to
|
|
25
25
|
* probe the calls SFU VM's own state over SSH during planning, so
|
|
26
|
-
* `platform-diff.ts`'s `planSfu`
|
|
27
|
-
* `DeclaredPlatform.sfu.enabled` is true, rather than
|
|
28
|
-
* live resource the way every other system here does. It is still part of
|
|
26
|
+
* `platform-diff.ts`'s `planSfu` plans its step from a recorded
|
|
27
|
+
* fingerprint when `DeclaredPlatform.sfu.enabled` is true, rather than
|
|
28
|
+
* diffing against a live resource the way every other system here does. It is still part of
|
|
29
29
|
* this union so `StepAction.system` (`diff.ts`) can name it. `'edge'`
|
|
30
30
|
* (#317) is the same: a edge key has no live lookup of its own, the same
|
|
31
31
|
* shape `ai`/`files` entry keys already follow.
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1008,9 +1008,13 @@ export class FoundryPlanApply {
|
|
|
1008
1008
|
store.getServiceIds(),
|
|
1009
1009
|
]);
|
|
1010
1010
|
const sfuBlocked = await this.sfuBlockedReason(platform);
|
|
1011
|
+
const callsOrgSlugs = (await this.store.list())
|
|
1012
|
+
.filter((org) => org.calls.enabled)
|
|
1013
|
+
.map((org) => org.slug);
|
|
1011
1014
|
const detail = computePlatformPlanDetail(platform, live, {
|
|
1012
1015
|
jobsObservable: readers.coolify !== undefined,
|
|
1013
1016
|
sfuBlocked,
|
|
1017
|
+
callsOrgSlugs,
|
|
1014
1018
|
recordedIds: serviceIds,
|
|
1015
1019
|
blocked: (await executor.blockedSteps?.(platform, serviceIds)) ?? {},
|
|
1016
1020
|
drifted: (await executor.driftedSteps?.(platform, serviceIds)) ?? {},
|
|
@@ -99,6 +99,11 @@ export declare function versionNotAppliedLine(service: string, appLive: boolean,
|
|
|
99
99
|
* the same `platform:<service>` shape as {@link PLATFORM_AUTH_KEY}.
|
|
100
100
|
*/
|
|
101
101
|
export declare const PLATFORM_ARCHON_KEY = "platform:archon";
|
|
102
|
+
/**
|
|
103
|
+
* The fingerprint (`callsSfuAppliedFingerprint`) of the last successful `sfu`
|
|
104
|
+
* apply. `planSfu` plans the step again when it differs from the desired one.
|
|
105
|
+
*/
|
|
106
|
+
export declare const PLATFORM_SFU_APPLIED_SCOPE_KEY = "sfu_applied_fingerprint";
|
|
102
107
|
/**
|
|
103
108
|
* #175: the hostname and `gitRef` the last successful Archon run applied.
|
|
104
109
|
* `planArchon` plans the step again when either differs from the declared
|
|
@@ -922,6 +927,11 @@ export interface PlanPlatformOptions {
|
|
|
922
927
|
jobsObservable?: boolean;
|
|
923
928
|
/** Why the shared SFU is blocked, or `null` when it is not (default `null`). */
|
|
924
929
|
sfuBlocked?: string | null;
|
|
930
|
+
/**
|
|
931
|
+
* The slugs of orgs with `calls.enabled`. With it, `planSfu` plans the step
|
|
932
|
+
* only when the recorded fingerprint differs; absent, it always plans it.
|
|
933
|
+
*/
|
|
934
|
+
callsOrgSlugs?: readonly string[];
|
|
925
935
|
/** The platform's recorded resource/scope ids (default `{}`). */
|
|
926
936
|
recordedIds?: Readonly<Record<string, string>>;
|
|
927
937
|
/** Per-step block reasons the executor reports (default `{}`). */
|
|
@@ -4,6 +4,7 @@ import { APP_ENTRY_KEY } from '../org.js';
|
|
|
4
4
|
import { VERSION_CHANGE_KIND, } from '../plan.js';
|
|
5
5
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
6
6
|
import { legacyAppHostname } from './hostnames.js';
|
|
7
|
+
import { callsSfuAppliedFingerprint } from './platform-sfu.js';
|
|
7
8
|
import { withStepSystem } from './step-system.js';
|
|
8
9
|
/**
|
|
9
10
|
* The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
|
|
@@ -122,6 +123,11 @@ export function versionNotAppliedLine(service, appLive, applied, pinned) {
|
|
|
122
123
|
* the same `platform:<service>` shape as {@link PLATFORM_AUTH_KEY}.
|
|
123
124
|
*/
|
|
124
125
|
export const PLATFORM_ARCHON_KEY = 'platform:archon';
|
|
126
|
+
/**
|
|
127
|
+
* The fingerprint (`callsSfuAppliedFingerprint`) of the last successful `sfu`
|
|
128
|
+
* apply. `planSfu` plans the step again when it differs from the desired one.
|
|
129
|
+
*/
|
|
130
|
+
export const PLATFORM_SFU_APPLIED_SCOPE_KEY = 'sfu_applied_fingerprint';
|
|
125
131
|
/**
|
|
126
132
|
* #175: the hostname and `gitRef` the last successful Archon run applied.
|
|
127
133
|
* `planArchon` plans the step again when either differs from the declared
|
|
@@ -1056,7 +1062,7 @@ export function uptimeRecorded(recordedIds) {
|
|
|
1056
1062
|
* unchanged).
|
|
1057
1063
|
*/
|
|
1058
1064
|
export function planPlatform(platform, live, options = {}) {
|
|
1059
|
-
const { jobsObservable = true, sfuBlocked = null, recordedIds = {}, blocked = {}, drifted = {}, } = options;
|
|
1065
|
+
const { jobsObservable = true, sfuBlocked = null, callsOrgSlugs, recordedIds = {}, blocked = {}, drifted = {}, } = options;
|
|
1060
1066
|
const byKey = new Map(live.map((r) => [resourceKey(r.system, r.kind, r.key), r]));
|
|
1061
1067
|
const claimed = new Set();
|
|
1062
1068
|
const plannedSteps = [];
|
|
@@ -1071,7 +1077,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1071
1077
|
// A blocked SFU is reported in `unmanaged`, not planned: a step the
|
|
1072
1078
|
// executor must refuse would halt every step after it (`diff.ts` does the
|
|
1073
1079
|
// same for a blocked Agora).
|
|
1074
|
-
const sfu = platform.sfu.enabled && sfuBlocked ? null : planSfu(platform);
|
|
1080
|
+
const sfu = platform.sfu.enabled && sfuBlocked ? null : planSfu(platform, callsOrgSlugs, recordedIds);
|
|
1075
1081
|
if (sfu)
|
|
1076
1082
|
plannedSteps.push(sfu);
|
|
1077
1083
|
// #478, F4.dns: first of the services, ahead of auth. Edge is the only
|
|
@@ -1505,20 +1511,29 @@ function planBackups(byKey, claimed, recordedIds, executorMissing, storageDrifte
|
|
|
1505
1511
|
/**
|
|
1506
1512
|
* The platform's shared calls SFU (#170), keyed `sfu:livekit:platform` --
|
|
1507
1513
|
* there is exactly one, so `key` names the platform itself rather than a
|
|
1508
|
-
* domain or an org id
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
function planSfu(platform) {
|
|
1514
|
+
* domain or an org id. It is never matched against a live resource: there is
|
|
1515
|
+
* no cheap way to read the SFU VM's own state over SSH during planning
|
|
1516
|
+
* (`LiveResource`'s doc comment on the `sfu` system). Instead the executor
|
|
1517
|
+
* records {@link callsSfuAppliedFingerprint} after a successful apply, and
|
|
1518
|
+
* with `callsOrgSlugs` the step is planned only when that record differs
|
|
1519
|
+
* from the desired one (new org set, changed calls files, new
|
|
1520
|
+
* `sfu.connectionRevision`). Without `callsOrgSlugs` it is planned whenever
|
|
1521
|
+
* `platform.sfu.enabled` is true. Planning it needlessly is safe:
|
|
1522
|
+
* `applyCallsSfu` is built to be re-run.
|
|
1523
|
+
*/
|
|
1524
|
+
function planSfu(platform, callsOrgSlugs, recordedIds) {
|
|
1520
1525
|
if (!platform.sfu.enabled)
|
|
1521
1526
|
return null;
|
|
1527
|
+
if (callsOrgSlugs &&
|
|
1528
|
+
recordedIds[PLATFORM_SFU_APPLIED_SCOPE_KEY] ===
|
|
1529
|
+
callsSfuAppliedFingerprint({
|
|
1530
|
+
orgSlugs: callsOrgSlugs,
|
|
1531
|
+
...(platform.sfu.connectionRevision
|
|
1532
|
+
? { connectionRevision: platform.sfu.connectionRevision }
|
|
1533
|
+
: {}),
|
|
1534
|
+
})) {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1522
1537
|
const action = {
|
|
1523
1538
|
system: 'sfu',
|
|
1524
1539
|
kind: 'livekit',
|
|
@@ -79,10 +79,9 @@ export interface OrgPlatformReadiness {
|
|
|
79
79
|
aiUrl?: string | null;
|
|
80
80
|
/**
|
|
81
81
|
* The shared calls SFU (#170) an org's own calls Matrix stack depends on.
|
|
82
|
-
* Declared is enough here:
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* SSH during planning"), so "applied" has no separate signal to check.
|
|
82
|
+
* Declared is enough here: `platform-diff.ts`'s `planSfu` plans the step
|
|
83
|
+
* from a recorded fingerprint, which is no signal an org's plan could
|
|
84
|
+
* read, so "applied" has no separate check.
|
|
86
85
|
*/
|
|
87
86
|
callsReady: boolean;
|
|
88
87
|
/** The platform's own sign-in (#168), which every org's own ZITADEL org lives inside. */
|
|
@@ -805,7 +805,7 @@ export declare function createAiManagementKeyMinter(options: {
|
|
|
805
805
|
* `_FILE` (#209 mints and pushes this once it lands) and `LETTERMINT_API_TOKEN`
|
|
806
806
|
* (#210: `wtfalch/auth#138` moved the broker's account mail onto
|
|
807
807
|
* `@wtfalch/mailer`'s Lettermint transport, so this is the env name the
|
|
808
|
-
* broker actually reads now, not
|
|
808
|
+
* broker actually reads now, not the earlier provider's token). Read from the broker's
|
|
809
809
|
* own live env, not a recorded scope id: whichever of #209/#210 lands first
|
|
810
810
|
* (or a human, setting either directly on the app) is enough to clear that
|
|
811
811
|
* one input, in either order, with no coupling to this step's own execution.
|
|
@@ -9,8 +9,8 @@ import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrSta
|
|
|
9
9
|
import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
|
|
10
10
|
import { healthCheckSettings } from './health-check-settings.js';
|
|
11
11
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
12
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
13
|
-
import { applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
12
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
13
|
+
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
14
14
|
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
15
15
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
16
16
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
@@ -644,7 +644,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
644
644
|
return provisionPlatformBackupStorage(context, this.backup);
|
|
645
645
|
}
|
|
646
646
|
if (action.system === 'sfu' && action.kind === 'livekit') {
|
|
647
|
-
return this.executeSfu();
|
|
647
|
+
return this.executeSfu(context);
|
|
648
648
|
}
|
|
649
649
|
throw new Error(`platform step ${action.system}:${action.kind} is not built yet`);
|
|
650
650
|
}
|
|
@@ -670,7 +670,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
670
670
|
* Sorted by slug so a re-run with the same set of enabled orgs is a no-op
|
|
671
671
|
* even before `applyCallsSfuPlatformStep`'s own sort (its doc comment).
|
|
672
672
|
*/
|
|
673
|
-
async executeSfu() {
|
|
673
|
+
async executeSfu(context) {
|
|
674
674
|
if (!this.sfu) {
|
|
675
675
|
throw new Error('platform step sfu:livekit is not built yet (#170: pass `sfu` to FoundryPlatformStepExecutor)');
|
|
676
676
|
}
|
|
@@ -680,6 +680,13 @@ export class FoundryPlatformStepExecutor {
|
|
|
680
680
|
.map((org) => org.slug)
|
|
681
681
|
.sort();
|
|
682
682
|
await applyCallsSfuPlatformStep(this.sfu, orgSlugs);
|
|
683
|
+
// Only after the apply succeeded: a failed apply records nothing, so the
|
|
684
|
+
// next plan shows the step again.
|
|
685
|
+
const { connectionRevision } = context.platform.sfu;
|
|
686
|
+
context.recordScopeId(PLATFORM_SFU_APPLIED_SCOPE_KEY, callsSfuAppliedFingerprint({
|
|
687
|
+
orgSlugs,
|
|
688
|
+
...(connectionRevision ? { connectionRevision } : {}),
|
|
689
|
+
}));
|
|
683
690
|
return undefined;
|
|
684
691
|
}
|
|
685
692
|
async coolifyJobs(action, context) {
|
|
@@ -1150,7 +1157,7 @@ async function liveLoginClientTokenHash(coolify, appId) {
|
|
|
1150
1157
|
* `IAM_LOGIN_CLIENT` machine user (never the admin one, which is
|
|
1151
1158
|
* `IAM_OWNER`) and delivers its token to both apps, once the broker's
|
|
1152
1159
|
* other required inputs are already met.
|
|
1153
|
-
* - ~~The broker never receives
|
|
1160
|
+
* - ~~The broker never receives the earlier mail provider's token either, so it will not
|
|
1154
1161
|
* start on `AUTH_APPS` alone.~~ Closed by #210, once `wtfalch/auth#138`
|
|
1155
1162
|
* moved the broker's account mail onto `@wtfalch/mailer`'s Lettermint
|
|
1156
1163
|
* transport: this function now reads the key back from
|
|
@@ -2029,8 +2036,8 @@ const PLATFORM_MAIL_ZITADEL_PROJECT_NAME = 'mail';
|
|
|
2029
2036
|
* instance-bootstrap helper `wtfalch-manage` already uses for the hand-built
|
|
2030
2037
|
* `mail.wtfalch.dev`: that plan also wires an `AcmeProvider` and `DnsServer`
|
|
2031
2038
|
* for Stalwart's own DNS-01 ACME challenge, and an `MtaRoute` that relays
|
|
2032
|
-
* every outbound message through
|
|
2033
|
-
* mail off
|
|
2039
|
+
* every outbound message through the earlier mail provider -- the estate moved transactional
|
|
2040
|
+
* mail off it onto Lettermint before this issue (`@wtfalch/mailer` ADR
|
|
2034
2041
|
* 0006), and this step provisions no Cloudflare DNS token for Stalwart's own
|
|
2035
2042
|
* ACME either. Calling it as-is would mint an outbound relay this platform
|
|
2036
2043
|
* does not want and a credential requirement this issue does not cover, so
|
|
@@ -2042,7 +2049,7 @@ const PLATFORM_MAIL_ZITADEL_PROJECT_NAME = 'mail';
|
|
|
2042
2049
|
* William's review"):
|
|
2043
2050
|
* - **No ACME/TLS for the SMTP listener.** `mx.<apex>` answers with
|
|
2044
2051
|
* Stalwart's own self-signed certificate until a follow-up wires a real
|
|
2045
|
-
* one (`mailObjects`'s `AcmeProvider`/`DnsServer` shape, minus the
|
|
2052
|
+
* one (`mailObjects`'s `AcmeProvider`/`DnsServer` shape, minus the earlier relay
|
|
2046
2053
|
* relay it also carries).
|
|
2047
2054
|
* - **The Lettermint team token the org app step reads still comes from
|
|
2048
2055
|
* wherever the host already reads it, not held keys.** Needs
|
|
@@ -5044,7 +5051,7 @@ async function findOrCreatePlatformApp(coolify, name, environment, create) {
|
|
|
5044
5051
|
* `_FILE` (#209 mints and pushes this once it lands) and `LETTERMINT_API_TOKEN`
|
|
5045
5052
|
* (#210: `wtfalch/auth#138` moved the broker's account mail onto
|
|
5046
5053
|
* `@wtfalch/mailer`'s Lettermint transport, so this is the env name the
|
|
5047
|
-
* broker actually reads now, not
|
|
5054
|
+
* broker actually reads now, not the earlier provider's token). Read from the broker's
|
|
5048
5055
|
* own live env, not a recorded scope id: whichever of #209/#210 lands first
|
|
5049
5056
|
* (or a human, setting either directly on the app) is enough to clear that
|
|
5050
5057
|
* one input, in either order, with no coupling to this step's own execution.
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
*/
|
|
38
38
|
import { type LivekitCredentials } from '../calls-sfu-keys.js';
|
|
39
39
|
import { type CallsSfuSshSession } from '../calls-sfu-ssh-adapter.js';
|
|
40
|
+
/**
|
|
41
|
+
* What the last successful `sfu` apply is recorded as
|
|
42
|
+
* (`PLATFORM_SFU_APPLIED_SCOPE_KEY`): a SHA-256 hex digest of foundry's calls
|
|
43
|
+
* files, the sorted distinct org slugs and the connection revision. It holds
|
|
44
|
+
* no secret and does not depend on slug order or duplicates.
|
|
45
|
+
*/
|
|
46
|
+
export declare function callsSfuAppliedFingerprint(input: {
|
|
47
|
+
orgSlugs: readonly string[];
|
|
48
|
+
connectionRevision?: string;
|
|
49
|
+
}): string;
|
|
40
50
|
/** The SFU VM's own address, SSH credentials and host key fingerprint -- held keys a human stored ahead of time. */
|
|
41
51
|
export interface CallsSfuConnection {
|
|
42
52
|
address: string;
|