@wtfalch/foundry 0.29.0 → 0.30.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-ssh-client.d.ts +11 -0
- package/dist/calls-sfu-ssh-client.js +65 -11
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/org.d.ts +88 -0
- package/dist/org.js +68 -0
- package/dist/plan/agora.d.ts +13 -0
- package/dist/plan/agora.js +15 -0
- package/dist/plan/chat-matrix.d.ts +31 -2
- package/dist/plan/chat-matrix.js +40 -6
- package/dist/plan/diff.d.ts +45 -2
- package/dist/plan/diff.js +162 -10
- package/dist/plan/executor.d.ts +93 -0
- package/dist/plan/executor.js +345 -9
- package/dist/plan/index.d.ts +5 -2
- package/dist/plan/index.js +3 -1
- package/dist/plan/plan-apply.js +1 -0
- package/dist/plan/platform-diff.d.ts +9 -0
- package/dist/plan/platform-diff.js +15 -3
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-org.js +4 -1
- package/dist/plan/platform-providers.d.ts +1 -1
- package/dist/plan/platform-providers.js +79 -22
- package/dist/plan/providers-readers.d.ts +14 -1
- package/dist/plan/providers-readers.js +25 -2
- package/dist/plan/step-system.js +1 -0
- package/dist/plan/version-status.d.ts +200 -0
- package/dist/plan/version-status.js +213 -0
- package/dist/plan/zitadel-login-policy.d.ts +23 -0
- package/dist/plan/zitadel-login-policy.js +103 -0
- package/dist/plan/zitadel-machine-user.d.ts +1 -1
- package/dist/plan/zitadel-machine-user.js +34 -0
- package/dist/platform.d.ts +1 -1
- package/package.json +3 -2
package/dist/plan/diff.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
|
-
import { APP_ENTRY_KEY, ownerMailboxRecorded, ownerMailboxes } from '../org.js';
|
|
3
|
-
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
2
|
+
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, ownerMailboxRecorded, ownerMailboxes, } from '../org.js';
|
|
3
|
+
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, chatCallsApplied, chatHasUpstreams, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
4
4
|
import { BOULE_BROKER_ENTRY_KEY, BOULE_MAIL_TOKEN_ENV, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
|
-
import { chatInternalNames } from './chat-matrix.js';
|
|
6
|
+
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
7
7
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
8
8
|
import { planEdgeReservedLabels, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
9
9
|
import { planFilesKeys } from './entry-files-key.js';
|
|
@@ -145,7 +145,12 @@ function liveResourceName(r) {
|
|
|
145
145
|
* app-creating step below: {@link planLoginHost}, the org's own company
|
|
146
146
|
* login host -- also idempotent on the *recorded* resource id, since
|
|
147
147
|
* `live` has no notion of a ZITADEL trusted domain or a login binding
|
|
148
|
-
* either.
|
|
148
|
+
* either. #444a adds one more fact, in a different shape from every other
|
|
149
|
+
* addition here: {@link planOrgPostgres}, the org's one shared Postgres
|
|
150
|
+
* server plus whichever of `org.ts`'s `ORG_SERVICE_SCHEMAS` (nine today)
|
|
151
|
+
* this org actually runs and has not yet recorded -- idempotent on the
|
|
152
|
+
* *recorded* resource id like {@link planOwners}, but never folded into
|
|
153
|
+
* `plannedSteps`: its own {@link OrgPostgresPlan} field says why. Everything
|
|
149
154
|
* else in `live` that none of these eighteen singletons and however many
|
|
150
155
|
* declared issuer apps claim is unmanaged. The full field-by-field provisioning surface (SSO,
|
|
151
156
|
* guests, calls on the org's own domain, adopted/bespoke apps, the actual
|
|
@@ -164,7 +169,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
164
169
|
const issuer = planIssuer(org, byKey, claimed, platformReadiness.authReady, isPlatformOrg);
|
|
165
170
|
if (issuer.step)
|
|
166
171
|
plannedSteps.push(issuer.step);
|
|
167
|
-
plannedSteps.push(...planOwners(org, recordedIds, platformReadiness.authReady,
|
|
172
|
+
plannedSteps.push(...planOwners(org, recordedIds, platformReadiness.authReady, platformReadiness.authIssuer));
|
|
168
173
|
// #267: after the ZITADEL org step, before any app's own issuer
|
|
169
174
|
// application. Apps created later in this run are not in the binding yet;
|
|
170
175
|
// the next run replans this step and repoints them.
|
|
@@ -176,6 +181,11 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
176
181
|
platformAuthHostnameFrom('error' in agoraHost ? null : agoraHost.issuer), isPlatformOrg);
|
|
177
182
|
if (loginHost.step)
|
|
178
183
|
plannedSteps.push(loginHost.step);
|
|
184
|
+
// #444a: the org's one shared Postgres server and its fixed schema
|
|
185
|
+
// topology -- dependent on nothing else planned here, but its own
|
|
186
|
+
// `OrgPostgresPlan`, never `plannedSteps` ({@link OrgPostgresPlan}'s own
|
|
187
|
+
// doc comment says why).
|
|
188
|
+
const orgPostgres = planOrgPostgres(org, recordedIds);
|
|
179
189
|
// Planned ahead of everything that needs the zone live (#238); its own step
|
|
180
190
|
// is pushed just below, before mail.
|
|
181
191
|
const zone = planZone(org, byKey, claimed);
|
|
@@ -230,6 +240,10 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
230
240
|
: callsTokenHostFrom(withRecordedMessagingProject(agoraHost, recordedIds)));
|
|
231
241
|
if (callsService.step)
|
|
232
242
|
plannedSteps.push(callsService.step);
|
|
243
|
+
// After both apps it writes to: chat's token and the calls service's chat address.
|
|
244
|
+
const callsChat = planCallsChat(org, recordedIds, chat.step !== null || callsService.step !== null, chat.blocked !== null || calls.blocked !== null || callsService.blocked !== null);
|
|
245
|
+
if (callsChat.step)
|
|
246
|
+
plannedSteps.push(callsChat.step);
|
|
233
247
|
// #251: same mechanism as the platform's own version-change lines -- see
|
|
234
248
|
// `planPlatform`'s own block in `platform-diff.ts`. Synapse and MAS never
|
|
235
249
|
// patch a running app's image tag once created, so this is
|
|
@@ -507,6 +521,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
507
521
|
unmanaged.push({ description: calls.blocked, key: 'coolify:calls' });
|
|
508
522
|
if (chat.blocked)
|
|
509
523
|
unmanaged.push({ description: chat.blocked, key: 'coolify:chat' });
|
|
524
|
+
if (callsChat.blocked) {
|
|
525
|
+
unmanaged.push({ description: callsChat.blocked, key: 'coolify:calls-chat' });
|
|
526
|
+
}
|
|
510
527
|
if (callsService.blocked) {
|
|
511
528
|
unmanaged.push({ description: callsService.blocked, key: 'coolify:calls-service' });
|
|
512
529
|
}
|
|
@@ -541,6 +558,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
541
558
|
plannedSteps: plannedSteps.map(withStepSystem),
|
|
542
559
|
unmanaged,
|
|
543
560
|
versionChanges,
|
|
561
|
+
orgPostgres,
|
|
544
562
|
};
|
|
545
563
|
}
|
|
546
564
|
/**
|
|
@@ -641,12 +659,20 @@ function planIssuer(org, byKey, claimed, authReady, isPlatformOrg = false) {
|
|
|
641
659
|
};
|
|
642
660
|
}
|
|
643
661
|
claimed.add(key);
|
|
662
|
+
// #517: both sides on the domain says nothing about which domain. The live
|
|
663
|
+
// issuer's recorded host differing from the declared domain's login host is
|
|
664
|
+
// a domain change, and moves the issuer too.
|
|
665
|
+
const liveHost = typeof live.config.host === 'string' ? live.config.host : null;
|
|
666
|
+
const domainMoved = Boolean(live.config.usesDomain) &&
|
|
667
|
+
desiredUsesDomain &&
|
|
668
|
+
Boolean(org.domain) &&
|
|
669
|
+
liveHost !== null &&
|
|
670
|
+
liveHost !== loginHostnameFor(org);
|
|
644
671
|
// The platform's own org already signs in on the shared host: no move. A
|
|
645
|
-
// live issuer that does not say
|
|
646
|
-
// moves one) is not read as a different one either.
|
|
672
|
+
// live issuer that does not say is not read as a different one either.
|
|
647
673
|
if (isPlatformOrg ||
|
|
648
674
|
live.config.usesDomain === undefined ||
|
|
649
|
-
Boolean(live.config.usesDomain) === desiredUsesDomain) {
|
|
675
|
+
(Boolean(live.config.usesDomain) === desiredUsesDomain && !domainMoved)) {
|
|
650
676
|
return { step: null, blocked: null };
|
|
651
677
|
}
|
|
652
678
|
const domain = org.domain ?? 'the org domain';
|
|
@@ -925,6 +951,50 @@ function loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned)
|
|
|
925
951
|
}
|
|
926
952
|
return null;
|
|
927
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Whether `org` runs the service {@link schema} belongs to, so
|
|
956
|
+
* {@link planOrgPostgres} reports a schema missing only once the org would
|
|
957
|
+
* actually need it -- CMS's, chat's and calls' entries follow their own
|
|
958
|
+
* on/off switch, the same ones this file already gates their own
|
|
959
|
+
* `coolify:*` steps on (`org.ts`'s `SiteConfig.enabled`, `ChatConfig.enabled`,
|
|
960
|
+
* `CallsConfig.enabled`); Boule's is unconditional, matching `org.ts`'s own
|
|
961
|
+
* "every org gets one" rule; the app schema follows whether any
|
|
962
|
+
* `hosting.entries` entry declares `database: true` (`coolifyDatabase`'s own
|
|
963
|
+
* gate) rather than `hosting.enabled` alone, since a hosting org with no
|
|
964
|
+
* database-bearing entry has nothing for this schema to hold.
|
|
965
|
+
*/
|
|
966
|
+
function orgServiceSchemaEnabled(org, schema) {
|
|
967
|
+
if (schema.schema === 'cms')
|
|
968
|
+
return org.site.enabled;
|
|
969
|
+
if (schema.schema.startsWith('chat_'))
|
|
970
|
+
return org.chat.enabled;
|
|
971
|
+
if (schema.schema.startsWith('calls_'))
|
|
972
|
+
return org.calls.enabled;
|
|
973
|
+
if (schema.schema === 'app')
|
|
974
|
+
return org.hosting.entries.some((entry) => entry.database);
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* #444a: reports the org's one shared Postgres server as missing until
|
|
979
|
+
* `recordedIds` says otherwise, and its fixed schema topology as missing for
|
|
980
|
+
* whichever schemas this org actually runs and has not yet recorded (see
|
|
981
|
+
* {@link orgServiceSchemaEnabled}) -- see {@link OrgPostgresPlan}'s own doc
|
|
982
|
+
* comment for why this is a `PlanDetail` field rather than `plannedSteps`
|
|
983
|
+
* entries. Nothing here is ever blocked (rule 2 does not apply: unlike chat
|
|
984
|
+
* or calls, the server and its schemas need no issuer, no ZITADEL org and no
|
|
985
|
+
* platform readiness to exist), and the server being missing never gates a
|
|
986
|
+
* schema being reported missing too -- a fresh org with every feature on
|
|
987
|
+
* reports both the same way #444a's own acceptance line asks for ("a fresh
|
|
988
|
+
* org plans one server and N schemas"), since nothing here actually depends
|
|
989
|
+
* on the server existing yet; that ordering is 444b's concern, once
|
|
990
|
+
* `plan/executor.ts` has something to run.
|
|
991
|
+
*/
|
|
992
|
+
function planOrgPostgres(org, recordedIds) {
|
|
993
|
+
return {
|
|
994
|
+
serverMissing: !recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY],
|
|
995
|
+
missingSchemas: ORG_SERVICE_SCHEMAS.filter((schema) => orgServiceSchemaEnabled(org, schema) && !recordedIds[schema.scopeKey]),
|
|
996
|
+
};
|
|
997
|
+
}
|
|
928
998
|
/** #230: restated from `executor.ts`'s `MAIL_ROLE_MAILBOXES_SCOPE_KEY`, same rule: comma-joined local parts already created. */
|
|
929
999
|
const MAIL_ROLE_MAILBOXES_SCOPE_KEY = 'mail_role_mailboxes';
|
|
930
1000
|
/** Restated from `executor.ts`, the same rule: the app id of the org tunnel's cloudflared connector. */
|
|
@@ -1260,13 +1330,22 @@ function planChat(org, byKey, claimed, recordedIds, reading, issuerPlanned) {
|
|
|
1260
1330
|
}
|
|
1261
1331
|
return none;
|
|
1262
1332
|
}
|
|
1263
|
-
const domain = org.domain ?? 'its domain';
|
|
1264
1333
|
if (!liveUsesDomain && desiredUsesDomain) {
|
|
1334
|
+
// #463: `usesDomain` is never fired by `domain` alone (#7 amendment 1),
|
|
1335
|
+
// but it can still be turned on with no domain recorded -- the step
|
|
1336
|
+
// itself has nowhere to re-key onto, so this is blocked rather than
|
|
1337
|
+
// planned (rule 2), not left for `coolifyChatRekey` to throw on.
|
|
1338
|
+
if (!org.domain) {
|
|
1339
|
+
return {
|
|
1340
|
+
step: null,
|
|
1341
|
+
blocked: `${org.slug}'s chat is blocked, not planned: usesDomain is on but the org has no domain to re-key onto`,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1265
1344
|
return {
|
|
1266
1345
|
step: {
|
|
1267
1346
|
action: { system: 'coolify', kind: 'chat', key: org.id, desired: { usesDomain: true } },
|
|
1268
1347
|
step: {
|
|
1269
|
-
description: `re-key ${org.slug}'s chat onto ${domain}, copying rooms and messages so history reads continuous`,
|
|
1348
|
+
description: `re-key ${org.slug}'s chat onto ${org.domain}, copying rooms and messages so history reads continuous`,
|
|
1270
1349
|
destroys: true,
|
|
1271
1350
|
migrates: true,
|
|
1272
1351
|
},
|
|
@@ -1431,6 +1510,79 @@ function planCallsService(org, byKey, claimed, callsReady, recordedIds, callsBlo
|
|
|
1431
1510
|
}
|
|
1432
1511
|
return { step: null, blocked: null };
|
|
1433
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* The token that lets the org's calls service ask its chat who is in a room
|
|
1515
|
+
* (`executor.ts`'s `coolifyCallsChat`). Chat off: nothing, unless the calls
|
|
1516
|
+
* service still carries the address, which is then removed so it does not
|
|
1517
|
+
* point at a chat that is gone. Both on: planned until the token is delivered
|
|
1518
|
+
* and both apps run with it, also for an org whose chat and calls service
|
|
1519
|
+
* already exist. An undelivered token is reported, never minted again (rule 1).
|
|
1520
|
+
* Silent while chat or the calls service is itself planned or blocked: they
|
|
1521
|
+
* report their own.
|
|
1522
|
+
*/
|
|
1523
|
+
function planCallsChat(org, recordedIds, upstreamPlanned, upstreamBlocked) {
|
|
1524
|
+
const none = { step: null, blocked: null };
|
|
1525
|
+
if (!org.calls.enabled)
|
|
1526
|
+
return none;
|
|
1527
|
+
const action = (desired) => ({
|
|
1528
|
+
system: 'coolify',
|
|
1529
|
+
kind: 'calls-chat',
|
|
1530
|
+
key: org.id,
|
|
1531
|
+
desired,
|
|
1532
|
+
});
|
|
1533
|
+
const callsServiceId = recordedIds[`coolify:calls-service:${org.id}`];
|
|
1534
|
+
if (!org.chat.enabled) {
|
|
1535
|
+
if (!callsServiceId || !recordedIds[CHAT_CALLS_APPLIED_SCOPE_KEY])
|
|
1536
|
+
return none;
|
|
1537
|
+
return {
|
|
1538
|
+
step: {
|
|
1539
|
+
action: action({ remove: true }),
|
|
1540
|
+
step: {
|
|
1541
|
+
description: `remove the chat address and token from ${org.slug}'s calls service (chat is off) and restart it`,
|
|
1542
|
+
destroys: false,
|
|
1543
|
+
migrates: false,
|
|
1544
|
+
},
|
|
1545
|
+
},
|
|
1546
|
+
blocked: null,
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
const tokenId = recordedIds[CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY];
|
|
1550
|
+
if (tokenId && !recordedIds[CHAT_CALLS_TOKEN_SCOPE_KEY]) {
|
|
1551
|
+
return {
|
|
1552
|
+
step: null,
|
|
1553
|
+
blocked: `${org.slug}'s calls-to-chat token is blocked, not planned: token ${tokenId} was minted but never written to both apps; remove the recorded key ${CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY} to write it again (chat-service's copy is reused, none is minted)`,
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
const guard = recordedIds[CHAT_CALLS_TOKEN_SCOPE_KEY];
|
|
1557
|
+
if (guard &&
|
|
1558
|
+
recordedIds[CHAT_CALLS_APPLIED_SCOPE_KEY] ===
|
|
1559
|
+
chatCallsApplied(guard, chatUpstreams(org.slug).chat)) {
|
|
1560
|
+
return none;
|
|
1561
|
+
}
|
|
1562
|
+
const ready = chatHasUpstreams(org, recordedIds) &&
|
|
1563
|
+
callsServiceId &&
|
|
1564
|
+
callsServiceHasName(org, recordedIds) &&
|
|
1565
|
+
recordedIds[CALLS_SERVICE_DEPLOYED_SCOPE_KEY];
|
|
1566
|
+
if (!ready && upstreamBlocked)
|
|
1567
|
+
return none;
|
|
1568
|
+
if (!ready && !upstreamPlanned) {
|
|
1569
|
+
return {
|
|
1570
|
+
step: null,
|
|
1571
|
+
blocked: `${org.slug}'s calls-to-chat token is blocked, not planned: chat or the calls service is not provisioned yet (apply them first)`,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
step: {
|
|
1576
|
+
action: action({}),
|
|
1577
|
+
step: {
|
|
1578
|
+
description: `give ${org.slug}'s chat service and calls service one internal token, point the calls service at chat, and redeploy both`,
|
|
1579
|
+
destroys: false,
|
|
1580
|
+
migrates: false,
|
|
1581
|
+
},
|
|
1582
|
+
},
|
|
1583
|
+
blocked: null,
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1434
1586
|
/**
|
|
1435
1587
|
* Deduped `ai.*` permissions offered to every organisation by default
|
|
1436
1588
|
* (Decision, this campaign's report): the minimum set that lets an org
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -712,6 +712,39 @@ export interface CallsSfuPort {
|
|
|
712
712
|
apiSecret: string;
|
|
713
713
|
} | null>;
|
|
714
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* #463: the cross-homeserver copy chat's forward re-key needs (design note,
|
|
717
|
+
* `docs/chat-rekey-design.md`, Option 1) -- copies every room, member and
|
|
718
|
+
* message from the org's platform-named homeserver onto its newly
|
|
719
|
+
* provisioned, domain-named one, inviting members under their new ids, so
|
|
720
|
+
* history reads continuous once the chat service is repointed
|
|
721
|
+
* (`coolifyChatRekey`).
|
|
722
|
+
*
|
|
723
|
+
* Optional on {@link FoundryStepExecutorPorts}, the same shape as
|
|
724
|
+
* `jobsScheduler`/`edge`/`mailCopy`: nothing in the estate has proven this
|
|
725
|
+
* mechanism against a real Synapse and MAS pair yet (issue #463's own Human
|
|
726
|
+
* step), so a host without one builds the executor without it and
|
|
727
|
+
* `coolify:chat`'s forward re-key throws `NotBuiltError` naming the gap,
|
|
728
|
+
* rather than running a copy nobody has watched work.
|
|
729
|
+
*
|
|
730
|
+
* Idempotent by `fromServerName`/`toServerName`: a room or message the
|
|
731
|
+
* implementation already copied for this pair is skipped, so a retried or
|
|
732
|
+
* resumed copy never duplicates history. Never returns or throws a secret,
|
|
733
|
+
* token or credential -- same rule as every other port here.
|
|
734
|
+
*/
|
|
735
|
+
export interface ChatRekeyPort {
|
|
736
|
+
copyHomeserver(input: {
|
|
737
|
+
orgSlug: string;
|
|
738
|
+
fromServerName: string;
|
|
739
|
+
fromInternalHost: string;
|
|
740
|
+
toServerName: string;
|
|
741
|
+
toInternalHost: string;
|
|
742
|
+
/** Aborted when foundry stops waiting for the copy. */
|
|
743
|
+
signal: AbortSignal;
|
|
744
|
+
}): Promise<{
|
|
745
|
+
roomsCopied: number;
|
|
746
|
+
}>;
|
|
747
|
+
}
|
|
715
748
|
/**
|
|
716
749
|
* The ZITADEL calls this executor makes. `findOrCreateOrg`/`findOrCreateProject`
|
|
717
750
|
* already exist on `@wtfalch/providers/zitadel`'s `ZitadelClient` (returning
|
|
@@ -871,6 +904,16 @@ export interface ZitadelPort {
|
|
|
871
904
|
allowSelfRegistration(orgId: string): Promise<{
|
|
872
905
|
changed: boolean;
|
|
873
906
|
}>;
|
|
907
|
+
/**
|
|
908
|
+
* Forces the org's login policy to `allowRegister: false`, writing the org
|
|
909
|
+
* its own policy even when it only inherits the instance default.
|
|
910
|
+
* Idempotent: a custom policy that already forbids registration is no
|
|
911
|
+
* write. `@wtfalch/providers` can only turn registration on, so
|
|
912
|
+
* `zitadel-login-policy.ts` is the real implementation.
|
|
913
|
+
*/
|
|
914
|
+
closeSelfRegistration(orgId: string): Promise<{
|
|
915
|
+
changed: boolean;
|
|
916
|
+
}>;
|
|
874
917
|
/** Grants another ZITADEL org access to a project (plan gap A9). Idempotent. */
|
|
875
918
|
findOrCreateProjectGrant(orgId: string, projectId: string, grantedOrgId: string): Promise<{
|
|
876
919
|
created: boolean;
|
|
@@ -1010,6 +1053,14 @@ export interface ZitadelPort {
|
|
|
1010
1053
|
* the binding holds; `zitadelApp` never sets it at creation.
|
|
1011
1054
|
*/
|
|
1012
1055
|
updateOidcAppLoginBaseUri(orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
|
|
1056
|
+
/**
|
|
1057
|
+
* Occupies `userName` in `orgId` with a machine user that has no role and
|
|
1058
|
+
* no token, so nobody can register under that name. Idempotent: a user
|
|
1059
|
+
* already holding the name, however it was made, is returned unchanged.
|
|
1060
|
+
*/
|
|
1061
|
+
reserveUserName(orgId: string, userName: string): Promise<{
|
|
1062
|
+
userId: string;
|
|
1063
|
+
}>;
|
|
1013
1064
|
/**
|
|
1014
1065
|
* Switches on "user info inside the ID token" for an already-created app,
|
|
1015
1066
|
* read-modify-write (`@wtfalch/providers`' `ZitadelClient.
|
|
@@ -1405,6 +1456,13 @@ export interface FoundryStepExecutorPorts {
|
|
|
1405
1456
|
* compilation over one dependency nothing else needs.
|
|
1406
1457
|
*/
|
|
1407
1458
|
mailCopy?: MailCopyPort;
|
|
1459
|
+
/**
|
|
1460
|
+
* #463: the cross-homeserver copy chat's forward re-key needs -- see
|
|
1461
|
+
* {@link ChatRekeyPort}'s own doc comment. Optional the same way
|
|
1462
|
+
* `mailCopy` is: without it `coolify:chat`'s forward re-key throws
|
|
1463
|
+
* `NotBuiltError` naming the gap instead of running an unproven copy.
|
|
1464
|
+
*/
|
|
1465
|
+
chatRekey?: ChatRekeyPort;
|
|
1408
1466
|
/** Flat, not a factory: `organisationId` is an explicit argument on every method of both ports (see each port's own doc comment; #177 dropped the `filesLimitsFor`/`aiLimitsFor` factories this used to need). */
|
|
1409
1467
|
filesOrganisation?: FilesOrganisationPort;
|
|
1410
1468
|
aiOrganisation?: AiOrganisationPort;
|
|
@@ -2338,6 +2396,26 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2338
2396
|
* No secret reaches an error: errors name the org and the missing input only.
|
|
2339
2397
|
*/
|
|
2340
2398
|
private coolifyChat;
|
|
2399
|
+
/**
|
|
2400
|
+
* `coolifyChat`'s forward re-key (#463, design note
|
|
2401
|
+
* `docs/chat-rekey-design.md` Option 1): a live chat recorded, and
|
|
2402
|
+
* `org.chat.usesDomain` flipped on. Provisions a second Synapse+MAS pair
|
|
2403
|
+
* named for the org's own domain (`chatRekeyedInternalNames`), copies
|
|
2404
|
+
* every room, member and message across through
|
|
2405
|
+
* {@link FoundryStepExecutorPorts.chatRekey}, repoints the existing chat
|
|
2406
|
+
* service at the new pair, and parks -- never deletes, so a stalled
|
|
2407
|
+
* migration can be rolled back by simply restarting them -- the original
|
|
2408
|
+
* Synapse and MAS apps. The chat service and its own database are never
|
|
2409
|
+
* re-created; only the Matrix half of the stack moves.
|
|
2410
|
+
*
|
|
2411
|
+
* Idempotent on recorded ids: new resources are found by name before they
|
|
2412
|
+
* are created, the new MAS's secrets are read back from its own envs, and
|
|
2413
|
+
* the copy, the repoint and the park each run once
|
|
2414
|
+
* ({@link CHAT_REKEY_COPIED_SCOPE_KEY}, {@link CHAT_REKEY_REPOINTED_SCOPE_KEY}).
|
|
2415
|
+
* The original pair keeps serving until the chat service is healthy on the
|
|
2416
|
+
* new one; a failure before that leaves it running.
|
|
2417
|
+
*/
|
|
2418
|
+
private coolifyChatRekey;
|
|
2341
2419
|
/**
|
|
2342
2420
|
* `plan/diff.ts`'s `planCallsService`: the org's own calls service --
|
|
2343
2421
|
* `wtfalch/calls`'s `packages/service`, wired to the calls Matrix stack
|
|
@@ -2352,6 +2430,21 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2352
2430
|
* 1); its own database is never touched from that branch.
|
|
2353
2431
|
*/
|
|
2354
2432
|
private coolifyCallsService;
|
|
2433
|
+
/**
|
|
2434
|
+
* `plan/diff.ts`'s `planCallsChat`: the one internal token that lets the
|
|
2435
|
+
* calls service ask chat who is in a room, and the chat address. The token
|
|
2436
|
+
* is `CHAT_INTERNAL_TOKEN` on chat-service and `CALLS_CHAT_TOKEN` on the
|
|
2437
|
+
* calls service, which gets `CALLS_CHAT_URL` in the same write: it refuses
|
|
2438
|
+
* to boot on one without the other. Chat-service's env is the only memory
|
|
2439
|
+
* of the token across a run, the way {@link ensureCallsMatrixSecrets} keeps
|
|
2440
|
+
* the MAS secrets on the MAS app, so it is generated only when that env has
|
|
2441
|
+
* none. Its id is recorded and persisted before either app holds it, and the
|
|
2442
|
+
* guard after both do (platform rule 1). Both apps are redeployed last; the
|
|
2443
|
+
* applied marker is recorded after that, so a run that fails in between is
|
|
2444
|
+
* planned and finished again. `remove` (chat is off) deletes both vars from
|
|
2445
|
+
* the calls service and restarts it.
|
|
2446
|
+
*/
|
|
2447
|
+
private coolifyCallsChat;
|
|
2355
2448
|
/**
|
|
2356
2449
|
* The seven ZITADEL token env vars the calls service needs to accept
|
|
2357
2450
|
* Agora's sign-in token (`calls/docs/member-auth.md`). Only called with all
|