@wtfalch/foundry 0.32.0 → 0.34.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/bootstrap/bootstrap.d.ts +18 -1
- package/dist/bootstrap/bootstrap.js +167 -28
- package/dist/bootstrap/declare.d.ts +5 -1
- package/dist/bootstrap/declare.js +3 -1
- package/dist/bootstrap/index.d.ts +1 -1
- package/dist/bootstrap/versions.d.ts +4 -0
- package/dist/calls-sfu.d.ts +10 -0
- package/dist/calls-sfu.js +41 -0
- package/dist/index.d.ts +18 -8
- package/dist/index.js +8 -3
- package/dist/migrations/0051_org_services.sql +13 -0
- package/dist/migrations/0052_platform_mail_relay.sql +8 -0
- package/dist/org.d.ts +24 -2
- package/dist/org.js +42 -0
- package/dist/plan/boule.d.ts +24 -1
- package/dist/plan/boule.js +29 -1
- package/dist/plan/calls-matrix.d.ts +9 -0
- package/dist/plan/calls-matrix.js +17 -0
- package/dist/plan/cloudflared-connector.d.ts +7 -1
- package/dist/plan/cloudflared-connector.js +14 -1
- package/dist/plan/diff.d.ts +32 -1
- package/dist/plan/diff.js +383 -26
- package/dist/plan/executor.d.ts +177 -7
- package/dist/plan/executor.js +738 -51
- package/dist/plan/health-check-settings.d.ts +28 -0
- package/dist/plan/health-check-settings.js +36 -0
- package/dist/plan/health-gaps.d.ts +33 -0
- package/dist/plan/health-gaps.js +89 -0
- package/dist/plan/health-targets.js +1 -1
- package/dist/plan/held-key-refs.d.ts +10 -0
- package/dist/plan/held-key-refs.js +10 -0
- package/dist/plan/index.d.ts +16 -6
- package/dist/plan/index.js +8 -3
- package/dist/plan/live.d.ts +5 -2
- package/dist/plan/live.js +15 -2
- package/dist/plan/locked-hostnames.d.ts +5 -2
- package/dist/plan/locked-hostnames.js +9 -1
- package/dist/plan/login-host.d.ts +62 -2
- package/dist/plan/login-host.js +104 -1
- package/dist/plan/mail-relay.d.ts +120 -0
- package/dist/plan/mail-relay.js +323 -0
- package/dist/plan/org-names.d.ts +24 -0
- package/dist/plan/org-names.js +52 -0
- package/dist/plan/own-diff.d.ts +169 -0
- package/dist/plan/own-diff.js +432 -0
- package/dist/plan/own-dns.d.ts +6 -0
- package/dist/plan/own-dns.js +15 -0
- package/dist/plan/own-limits.d.ts +22 -0
- package/dist/plan/own-limits.js +101 -0
- package/dist/plan/own-services.d.ts +193 -0
- package/dist/plan/own-services.js +167 -0
- package/dist/plan/own-sfu.d.ts +69 -0
- package/dist/plan/own-sfu.js +255 -0
- package/dist/plan/plan-apply.d.ts +101 -7
- package/dist/plan/plan-apply.js +216 -12
- package/dist/plan/platform-diff.d.ts +61 -12
- package/dist/plan/platform-diff.js +140 -34
- package/dist/plan/platform-org.d.ts +36 -0
- package/dist/plan/platform-org.js +96 -2
- package/dist/plan/platform-providers.d.ts +407 -12
- package/dist/plan/platform-providers.js +947 -422
- package/dist/plan/sign-in-org.d.ts +99 -0
- package/dist/plan/sign-in-org.js +151 -0
- package/dist/plan/zitadel-login-policy.js +1 -1
- package/dist/plan/zitadel-system-api.d.ts +22 -1
- package/dist/plan/zitadel-system-api.js +49 -8
- package/dist/plan.d.ts +18 -0
- package/dist/platform.d.ts +8 -0
- package/dist/slug.js +1 -0
- package/dist/store/postgres.js +51 -2
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +5 -0
- package/package.json +1 -1
- package/versions.json +3 -1
|
@@ -5,7 +5,9 @@ import { VERSION_CHANGE_KIND, } from '../plan.js';
|
|
|
5
5
|
import { versionChangeNeedsConfirmation } from './confirm-upgrade.js';
|
|
6
6
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
7
7
|
import { legacyAppHostname } from './hostnames.js';
|
|
8
|
+
import { PLATFORM_MAIL_RELAY_KEY, planMailRelay } from './mail-relay.js';
|
|
8
9
|
import { callsSfuAppliedFingerprint } from './platform-sfu.js';
|
|
10
|
+
import { ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY, signInDomainBlockedText } from './sign-in-org.js';
|
|
9
11
|
import { withStepSystem } from './step-system.js';
|
|
10
12
|
/**
|
|
11
13
|
* The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
|
|
@@ -22,6 +24,7 @@ export function toPlatformPlan(detail, ownerOrgId) {
|
|
|
22
24
|
steps: detail.plannedSteps.map((p) => p.step),
|
|
23
25
|
unmanaged: detail.unmanaged,
|
|
24
26
|
versionChanges: detail.versionChanges,
|
|
27
|
+
notes: detail.notes,
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
function resourceKey(system, kind, key) {
|
|
@@ -195,12 +198,11 @@ export const PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY = 'archon_deployment';
|
|
|
195
198
|
*/
|
|
196
199
|
export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_app';
|
|
197
200
|
/**
|
|
198
|
-
* The
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* has recorded it.
|
|
201
|
+
* The sign-in org id the last Archon apply set as `AUTH_ORGANIZATION_ID` and
|
|
202
|
+
* registered its broker entry under. `planArchon` plans the step again when
|
|
203
|
+
* {@link archonAuthOrganizationId} differs from it.
|
|
202
204
|
*/
|
|
203
|
-
export const
|
|
205
|
+
export const PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY = 'archon_applied_auth_org';
|
|
204
206
|
/**
|
|
205
207
|
* #475: whether the last successful Archon apply patched the app with
|
|
206
208
|
* `custom_network_aliases` and `is_consistent_container_name_enabled: false`
|
|
@@ -217,13 +219,15 @@ export const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = 'archon_applied_bootst
|
|
|
217
219
|
*/
|
|
218
220
|
export const PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY = 'archon_applied_rolling_update';
|
|
219
221
|
/**
|
|
220
|
-
* The
|
|
221
|
-
* {@link
|
|
222
|
-
*
|
|
222
|
+
* The sign-in org Archon accepts: the platform owner company's own
|
|
223
|
+
* ({@link PLATFORM_OWNER_ORG_SCOPE_KEY}), else the platform's `foundry` org for
|
|
224
|
+
* a platform with no owner company declared. Undefined when neither is recorded.
|
|
223
225
|
*/
|
|
224
|
-
export function
|
|
225
|
-
const
|
|
226
|
-
|
|
226
|
+
export function archonAuthOrganizationId(recorded) {
|
|
227
|
+
const owner = recorded[PLATFORM_OWNER_ORG_SCOPE_KEY];
|
|
228
|
+
if (owner && owner !== 'none')
|
|
229
|
+
return owner;
|
|
230
|
+
return recorded[PLATFORM_ZITADEL_ORG_SCOPE_KEY] || undefined;
|
|
227
231
|
}
|
|
228
232
|
/**
|
|
229
233
|
* #175: set to '1' before the step points the tunnel at `http_status:404`
|
|
@@ -244,6 +248,26 @@ export const PLATFORM_AUTH_SMTP_SCOPE_KEY = 'zitadel_platform_smtp_provider_id';
|
|
|
244
248
|
* requested, so the invite goes out once. `none` when no owner is declared.
|
|
245
249
|
*/
|
|
246
250
|
export const PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY = 'zitadel_platform_owner_invite_user_id';
|
|
251
|
+
/** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
|
|
252
|
+
export const PLATFORM_ZITADEL_ORG_SCOPE_KEY = 'zitadel_platform_org';
|
|
253
|
+
/**
|
|
254
|
+
* The id of the platform owner company's own sign-in organisation, made by
|
|
255
|
+
* `platform:auth` with the same name and primary domain a company apply uses,
|
|
256
|
+
* so that apply finds this org and makes no second one. `none` when the owner
|
|
257
|
+
* company is not declared. Archon signs people in from this org; the
|
|
258
|
+
* `foundry` org ({@link PLATFORM_ZITADEL_ORG_SCOPE_KEY}) holds no people.
|
|
259
|
+
*/
|
|
260
|
+
export const PLATFORM_OWNER_ORG_SCOPE_KEY = 'zitadel_platform_owner_org';
|
|
261
|
+
/** The primary domain `platform:auth` registered on the owner company's sign-in org. */
|
|
262
|
+
export const PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY = 'zitadel_platform_owner_org_domain';
|
|
263
|
+
/** The owner org's sign-in domain the sign-in server refused to make primary; set instead of failing, reported as blocked. */
|
|
264
|
+
export const PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY = 'zitadel_platform_owner_org_domain_blocked';
|
|
265
|
+
/**
|
|
266
|
+
* The sign-in org the recorded owner invite ({@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY})
|
|
267
|
+
* was made in. An invite recorded against another org (the `foundry` org, on a
|
|
268
|
+
* platform applied before the owner moved) plans the auth step again.
|
|
269
|
+
*/
|
|
270
|
+
export const PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY = 'zitadel_platform_owner_invite_org';
|
|
247
271
|
/**
|
|
248
272
|
* #394: the id of the role-less ZITADEL machine user that reserves the
|
|
249
273
|
* username `admin` in the platform's own org, so nobody can sign up under a
|
|
@@ -253,8 +277,6 @@ export const PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY = 'zitadel_platform_owner_invi
|
|
|
253
277
|
export const PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY = 'zitadel_platform_reserved_admin_username';
|
|
254
278
|
/** #394: recorded once the platform's own ZITADEL org has self-registration forced off. */
|
|
255
279
|
export const PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY = 'zitadel_platform_registration_closed';
|
|
256
|
-
/** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
|
|
257
|
-
export const PLATFORM_ZITADEL_ORG_SCOPE_KEY = 'zitadel_platform_org';
|
|
258
280
|
export const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = 'zitadel_platform_admin_user_id';
|
|
259
281
|
export const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_admin_user_undelivered';
|
|
260
282
|
/** #283: the created instance's id, recorded once its owner token is delivered. */
|
|
@@ -461,6 +483,26 @@ export const PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY = 'mail_applied_git_ref';
|
|
|
461
483
|
* no key and creates nothing in the sign-in server.
|
|
462
484
|
*/
|
|
463
485
|
export const PLATFORM_MAIL_ROUTE_SCOPE_KEY = 'mail_route_applied';
|
|
486
|
+
/**
|
|
487
|
+
* The host ports the platform Stalwart publishes, as the mapping string the
|
|
488
|
+
* last apply set on its app. Without the shared relay it binds SMTP port 25
|
|
489
|
+
* itself; with `mail.relay` it keeps only submission, because the relay owns
|
|
490
|
+
* port 25. A missing record means the mapping it was created with, `25:25,...`.
|
|
491
|
+
*/
|
|
492
|
+
export const PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY = 'mail_host_ports_applied';
|
|
493
|
+
export const PLATFORM_MAIL_HOST_PORTS_DIRECT = '25:25,465:465,587:587';
|
|
494
|
+
export const PLATFORM_MAIL_HOST_PORTS_RELAY = '465:465,587:587';
|
|
495
|
+
export function platformMailHostPorts(platform) {
|
|
496
|
+
return platform.mail.relay === true
|
|
497
|
+
? PLATFORM_MAIL_HOST_PORTS_RELAY
|
|
498
|
+
: PLATFORM_MAIL_HOST_PORTS_DIRECT;
|
|
499
|
+
}
|
|
500
|
+
function mailPortsApplied(platform, recordedIds) {
|
|
501
|
+
return ((recordedIds[PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY] ?? PLATFORM_MAIL_HOST_PORTS_DIRECT) ===
|
|
502
|
+
platformMailHostPorts(platform));
|
|
503
|
+
}
|
|
504
|
+
export { PLATFORM_MAIL_RELAY_KEY };
|
|
505
|
+
export { PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, } from './mail-relay.js';
|
|
464
506
|
export const PLATFORM_MAIL_ROUTE_WANTED = 'local-first';
|
|
465
507
|
/**
|
|
466
508
|
* #177: where the mail server answers on the Coolify network
|
|
@@ -1092,7 +1134,7 @@ export function uptimeRecorded(recordedIds) {
|
|
|
1092
1134
|
* unchanged).
|
|
1093
1135
|
*/
|
|
1094
1136
|
export function planPlatform(platform, live, options = {}) {
|
|
1095
|
-
const { jobsObservable = true, sfuBlocked = null, callsOrgSlugs, recordedIds = {}, blocked = {}, drifted = {}, } = options;
|
|
1137
|
+
const { jobsObservable = true, sfuBlocked = null, callsOrgSlugs, mailRoutes = [], mailPlatformDomains = [], mailRoutesBlocked = [], recordedIds = {}, blocked = {}, drifted = {}, } = options;
|
|
1096
1138
|
const byKey = new Map(live.map((r) => [resourceKey(r.system, r.kind, r.key), r]));
|
|
1097
1139
|
const claimed = new Set();
|
|
1098
1140
|
const plannedSteps = [];
|
|
@@ -1138,7 +1180,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1138
1180
|
// reconcile below can be planned in the *same* round hc first comes up,
|
|
1139
1181
|
// the same way files/mail/archon are planned in auth's own first round.
|
|
1140
1182
|
const hcReady = hc.step !== null || Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_HC_KEY)]);
|
|
1141
|
-
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? []);
|
|
1183
|
+
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite);
|
|
1142
1184
|
if (auth.step)
|
|
1143
1185
|
plannedSteps.push(auth.step);
|
|
1144
1186
|
// #174: the integrations host. Planned after auth; neither step reads the
|
|
@@ -1157,6 +1199,18 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1157
1199
|
const mail = planMail(platform, recordedIds, blocked.mail ?? dnsBlocked?.('mail server'), authReady);
|
|
1158
1200
|
if (mail.step)
|
|
1159
1201
|
plannedSteps.push(mail.step);
|
|
1202
|
+
// The shared relay owns host port 25 once `mail.relay` is on, so it runs after the
|
|
1203
|
+
// mail step that gives the port up.
|
|
1204
|
+
const relay = planMailRelay(platform, recordedIds, {
|
|
1205
|
+
routes: mailRoutes,
|
|
1206
|
+
platformDomains: mailPlatformDomains,
|
|
1207
|
+
executorBlocked: blocked.relay,
|
|
1208
|
+
mailReady: mail.step !== null ||
|
|
1209
|
+
(Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_MAIL_KEY)]) &&
|
|
1210
|
+
mailPortsApplied(platform, recordedIds)),
|
|
1211
|
+
});
|
|
1212
|
+
if (relay.step)
|
|
1213
|
+
plannedSteps.push(relay.step);
|
|
1160
1214
|
// #172: a throw halts every step after it, so this one goes late.
|
|
1161
1215
|
const ai = planAi(platform, recordedIds, blocked.ai);
|
|
1162
1216
|
if (ai.step)
|
|
@@ -1243,6 +1297,12 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1243
1297
|
// A blocked jobs scheduler is reported here, not planned: a step that
|
|
1244
1298
|
// throws every apply, or that can never be seen live and so re-plans
|
|
1245
1299
|
// forever, is worse than none.
|
|
1300
|
+
if (!platform.ownerOrgId) {
|
|
1301
|
+
unmanaged.push({
|
|
1302
|
+
description: "platform.ownerOrgId is not declared, so no org is the platform's own: no peer password or bridge secret is minted for any Boule and Archon reads no people",
|
|
1303
|
+
key: 'owner-org',
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1246
1306
|
if (jobs.blocked)
|
|
1247
1307
|
unmanaged.push({ description: jobs.blocked, key: 'jobs' });
|
|
1248
1308
|
if (platform.sfu.enabled && sfuBlocked) {
|
|
@@ -1253,6 +1313,13 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1253
1313
|
unmanaged.push({ description: auth.blocked, key: 'auth' });
|
|
1254
1314
|
if (mail.blocked)
|
|
1255
1315
|
unmanaged.push({ description: mail.blocked, key: 'mail' });
|
|
1316
|
+
if (relay.blocked)
|
|
1317
|
+
unmanaged.push({ description: relay.blocked, key: 'mail-relay' });
|
|
1318
|
+
if (platform.mail.relay === true) {
|
|
1319
|
+
for (const description of mailRoutesBlocked) {
|
|
1320
|
+
unmanaged.push({ description, key: 'mail-relay:route' });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1256
1323
|
if (integrations.blocked)
|
|
1257
1324
|
unmanaged.push({ description: integrations.blocked, key: 'integrations' });
|
|
1258
1325
|
if (files.blocked)
|
|
@@ -1284,7 +1351,12 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1284
1351
|
unmanaged.push({ description: heartbeats.blocked, key: 'heartbeats' });
|
|
1285
1352
|
for (const description of uptime.blocked)
|
|
1286
1353
|
unmanaged.push({ description, key: 'uptime' });
|
|
1287
|
-
return {
|
|
1354
|
+
return {
|
|
1355
|
+
plannedSteps: plannedSteps.map(withStepSystem),
|
|
1356
|
+
unmanaged,
|
|
1357
|
+
versionChanges,
|
|
1358
|
+
notes: uptime.notes,
|
|
1359
|
+
};
|
|
1288
1360
|
}
|
|
1289
1361
|
/**
|
|
1290
1362
|
* The platform's own Cloudflare zone for its shared apex (`platform.ts`'s
|
|
@@ -1703,9 +1775,12 @@ function planSfu(platform, callsOrgSlugs, recordedIds) {
|
|
|
1703
1775
|
* promising "this apply re-mints" while one of those is still missing would
|
|
1704
1776
|
* be wrong the moment it is read.
|
|
1705
1777
|
*/
|
|
1706
|
-
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = []) {
|
|
1778
|
+
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked) {
|
|
1707
1779
|
if (!platform.auth.enabled)
|
|
1708
1780
|
return { step: null, blocked: null };
|
|
1781
|
+
// C3/S2: standing notes. The invite and the domain wait on a person; they hold nothing else back.
|
|
1782
|
+
const standing = [ownerInviteBlocked, ownerDomainBlockedText(recordedIds)].filter((t) => Boolean(t));
|
|
1783
|
+
const standingNote = standing.length > 0 ? standing.join('; ') : null;
|
|
1709
1784
|
const key = resourceKey('coolify', 'app', PLATFORM_AUTH_KEY);
|
|
1710
1785
|
const connectorApplied = Boolean(recordedIds[PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY]);
|
|
1711
1786
|
// #211: a recorded app and connector with no recorded broker id is a
|
|
@@ -1739,11 +1814,18 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
1739
1814
|
// Mail and the owner's invite: a platform applied before they existed
|
|
1740
1815
|
// plans the step again, which only adds them.
|
|
1741
1816
|
Boolean(recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY]) &&
|
|
1742
|
-
Boolean(recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY]) &&
|
|
1817
|
+
(Boolean(recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY]) || Boolean(ownerInviteBlocked)) &&
|
|
1818
|
+
// The owner company's own sign-in org, and an invite made in it: a
|
|
1819
|
+
// platform whose owner was invited into the `foundry` org plans the step again.
|
|
1820
|
+
Boolean(recordedIds[PLATFORM_OWNER_ORG_SCOPE_KEY]) &&
|
|
1821
|
+
(Boolean(ownerInviteBlocked) ||
|
|
1822
|
+
recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY] === 'none' ||
|
|
1823
|
+
recordedIds[PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY] ===
|
|
1824
|
+
recordedIds[PLATFORM_OWNER_ORG_SCOPE_KEY]) &&
|
|
1743
1825
|
// The ZITADEL run options reach the container only through the auth
|
|
1744
1826
|
// step's redeploy, so a platform recorded before them still plans it.
|
|
1745
1827
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS) {
|
|
1746
|
-
return { step: null, blocked:
|
|
1828
|
+
return { step: null, blocked: standingNote };
|
|
1747
1829
|
}
|
|
1748
1830
|
// The admin key was minted but never reached the host. ZITADEL shows a
|
|
1749
1831
|
// token once, so it is lost: minting another would hide that, and skipping
|
|
@@ -1814,6 +1896,7 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
1814
1896
|
// 5: and only claim "this apply re-mints" when `otherInputsMissing` is
|
|
1815
1897
|
// actually empty -- see this function's own doc comment.
|
|
1816
1898
|
blocked: executorBlocked ??
|
|
1899
|
+
standingNote ??
|
|
1817
1900
|
loginClientNeedsMintNote(loginClientUndelivered, loginClientApplied, authDrifted, loginClientRecord, otherInputsMissing),
|
|
1818
1901
|
};
|
|
1819
1902
|
}
|
|
@@ -1879,7 +1962,8 @@ function planMail(platform, recordedIds, executorBlocked, authReady) {
|
|
|
1879
1962
|
// #177: a mail step applied before this key existed never recorded it, and
|
|
1880
1963
|
// org steps read it for Agora's mail upstream. Missing means unapplied.
|
|
1881
1964
|
recordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY] &&
|
|
1882
|
-
recordedIds[PLATFORM_MAIL_ROUTE_SCOPE_KEY] === PLATFORM_MAIL_ROUTE_WANTED
|
|
1965
|
+
recordedIds[PLATFORM_MAIL_ROUTE_SCOPE_KEY] === PLATFORM_MAIL_ROUTE_WANTED &&
|
|
1966
|
+
mailPortsApplied(platform, recordedIds)) {
|
|
1883
1967
|
return { step: null, blocked: null };
|
|
1884
1968
|
}
|
|
1885
1969
|
// Only the route is missing: say so, the app is not redeployed.
|
|
@@ -1887,7 +1971,8 @@ function planMail(platform, recordedIds, executorBlocked, authReady) {
|
|
|
1887
1971
|
recordedIds[PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY] &&
|
|
1888
1972
|
recordedIds[PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY] === platform.mail.hostname &&
|
|
1889
1973
|
recordedIds[PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY] === platform.mail.gitRef &&
|
|
1890
|
-
recordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY]
|
|
1974
|
+
recordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY] &&
|
|
1975
|
+
mailPortsApplied(platform, recordedIds);
|
|
1891
1976
|
// Stalwart's `x:ApiKey/set` shows a secret once. An id was minted but never
|
|
1892
1977
|
// reached the host: report it, plan nothing, the same rule {@link planAuth}
|
|
1893
1978
|
// follows for ZITADEL's own admin key.
|
|
@@ -2487,9 +2572,9 @@ function planHeartbeats(platform, recordedIds, hcReady, executorMissing, apiKeyM
|
|
|
2487
2572
|
* destructive step removes whatever of it is recorded (edge removes its KV
|
|
2488
2573
|
* namespace with the script).
|
|
2489
2574
|
*
|
|
2490
|
-
* The returned `blocked` lines are reported in `unmanaged
|
|
2491
|
-
*
|
|
2492
|
-
*
|
|
2575
|
+
* The returned `blocked` lines are reported in `unmanaged`. A secret whose
|
|
2576
|
+
* source does not exist yet blocks nothing: the step still delivers
|
|
2577
|
+
* everything that exists, so it comes back in `notes`.
|
|
2493
2578
|
*/
|
|
2494
2579
|
function planUptime(platform, recordedIds, executorBlocked, heldOptional, archonApplied, uptimeHeld = {
|
|
2495
2580
|
token: true,
|
|
@@ -2507,9 +2592,9 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2507
2592
|
});
|
|
2508
2593
|
if (!uptime.enabled) {
|
|
2509
2594
|
if (!uptimeRecorded(recordedIds))
|
|
2510
|
-
return { step: null, blocked: [] };
|
|
2595
|
+
return { step: null, blocked: [], notes: [] };
|
|
2511
2596
|
if (executorBlocked)
|
|
2512
|
-
return { step: null, blocked: [executorBlocked] };
|
|
2597
|
+
return { step: null, blocked: [executorBlocked], notes: [] };
|
|
2513
2598
|
return {
|
|
2514
2599
|
step: {
|
|
2515
2600
|
action: action({ remove: true }),
|
|
@@ -2520,6 +2605,7 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2520
2605
|
},
|
|
2521
2606
|
},
|
|
2522
2607
|
blocked: [],
|
|
2608
|
+
notes: [],
|
|
2523
2609
|
};
|
|
2524
2610
|
}
|
|
2525
2611
|
if (!uptime.tag || !uptime.sha256) {
|
|
@@ -2528,6 +2614,7 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2528
2614
|
blocked: [
|
|
2529
2615
|
'the uptime Worker is blocked, not planned: no artifact is pinned; declare uptime.tag (an Archon release tag) and uptime.sha256 (the digest of archon-uptime-worker.js at that tag)',
|
|
2530
2616
|
],
|
|
2617
|
+
notes: [],
|
|
2531
2618
|
};
|
|
2532
2619
|
}
|
|
2533
2620
|
if (!archonApplied) {
|
|
@@ -2536,16 +2623,25 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2536
2623
|
blocked: [
|
|
2537
2624
|
'the uptime Worker is blocked, not planned: platform:archon has not been applied yet',
|
|
2538
2625
|
],
|
|
2626
|
+
notes: [],
|
|
2539
2627
|
};
|
|
2540
2628
|
}
|
|
2541
2629
|
if (executorBlocked)
|
|
2542
|
-
return { step: null, blocked: [executorBlocked] };
|
|
2543
|
-
const
|
|
2630
|
+
return { step: null, blocked: [executorBlocked], notes: [] };
|
|
2631
|
+
const notes = [];
|
|
2544
2632
|
if (!heldOptional.includes('CHAT_ACCESS_TOKEN')) {
|
|
2545
|
-
|
|
2633
|
+
notes.push({
|
|
2634
|
+
key: 'uptime:CHAT_ACCESS_TOKEN',
|
|
2635
|
+
service: 'uptime',
|
|
2636
|
+
description: `the uptime Worker has no chat credential (CHAT_ACCESS_TOKEN): hold one under ${PLATFORM_UPTIME_CHAT_TOKEN_REF} and it is set on the next apply; until then it sends no chat alerts`,
|
|
2637
|
+
});
|
|
2546
2638
|
}
|
|
2547
2639
|
if (!heldOptional.includes('MAIL_API_KEY')) {
|
|
2548
|
-
|
|
2640
|
+
notes.push({
|
|
2641
|
+
key: 'uptime:MAIL_API_KEY',
|
|
2642
|
+
service: 'uptime',
|
|
2643
|
+
description: `the uptime Worker has no mail API key (MAIL_API_KEY): hold one under ${PLATFORM_UPTIME_MAIL_KEY_REF} and it is set on the next apply; until then it sends no alert email`,
|
|
2644
|
+
});
|
|
2549
2645
|
}
|
|
2550
2646
|
const hostname = platform.archon.hostname;
|
|
2551
2647
|
// Applied only once a run got to its end and recorded these: a Worker that
|
|
@@ -2561,7 +2657,7 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2561
2657
|
(recordedIds[PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY] ?? '') === uptimeHeld.vars &&
|
|
2562
2658
|
recordedIds[PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY] === uptimeSecretNames(heldOptional);
|
|
2563
2659
|
if (applied)
|
|
2564
|
-
return { step: null, blocked:
|
|
2660
|
+
return { step: null, blocked: [], notes };
|
|
2565
2661
|
return {
|
|
2566
2662
|
step: {
|
|
2567
2663
|
action: action({ hostname, gitRef: uptime.tag, sha256: uptime.sha256 }),
|
|
@@ -2571,7 +2667,8 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2571
2667
|
migrates: false,
|
|
2572
2668
|
},
|
|
2573
2669
|
},
|
|
2574
|
-
blocked:
|
|
2670
|
+
blocked: [],
|
|
2671
|
+
notes,
|
|
2575
2672
|
};
|
|
2576
2673
|
}
|
|
2577
2674
|
/**
|
|
@@ -2709,8 +2806,8 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted,
|
|
|
2709
2806
|
Boolean(recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY]) &&
|
|
2710
2807
|
recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ===
|
|
2711
2808
|
recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY] &&
|
|
2712
|
-
|
|
2713
|
-
(
|
|
2809
|
+
recordedIds[PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY] ===
|
|
2810
|
+
archonAuthOrganizationId(recordedIds) &&
|
|
2714
2811
|
// #475: a platform applied before the rolling-update fix keeps planning
|
|
2715
2812
|
// this step until a confirmed redeploy has cleared it. #480: this marker
|
|
2716
2813
|
// alone does not catch the live app's settings changing back afterward
|
|
@@ -2775,3 +2872,12 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted,
|
|
|
2775
2872
|
blocked: null,
|
|
2776
2873
|
};
|
|
2777
2874
|
}
|
|
2875
|
+
/** The blocked text for a refused owner-org sign-in domain, while it is recorded and not yet primary. */
|
|
2876
|
+
function ownerDomainBlockedText(recordedIds) {
|
|
2877
|
+
const blockedDomain = recordedIds[PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY];
|
|
2878
|
+
if (!blockedDomain)
|
|
2879
|
+
return null;
|
|
2880
|
+
if (recordedIds[PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY] === blockedDomain)
|
|
2881
|
+
return null;
|
|
2882
|
+
return signInDomainBlockedText('the platform owner', blockedDomain).replace(ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY);
|
|
2883
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DeclaredOrg } from '../org.js';
|
|
1
2
|
import type { DeclaredPlatform } from '../platform.js';
|
|
2
3
|
import type { AgoraHostValues } from './agora.js';
|
|
3
4
|
/**
|
|
@@ -61,6 +62,8 @@ export interface OrgPlatformReadiness {
|
|
|
61
62
|
* while `MAIL_DOMAIN_SCOPE_KEY` was still recorded as the apex.
|
|
62
63
|
*/
|
|
63
64
|
platformApex: string | null;
|
|
65
|
+
/** `DeclaredPlatform.ownerOrgId`, or `null` when no platform is declared or it is empty. The one fact "this is the platform's own org" rests on. */
|
|
66
|
+
platformOwnerOrgId: string | null;
|
|
64
67
|
/** `DeclaredPlatform.mail.lettermintProjectId`, `null` when undeclared or when no platform is declared. See {@link platformApex}'s own doc comment for why this is unconditional too. */
|
|
65
68
|
platformLettermintProjectId: string | null;
|
|
66
69
|
/** The files host (#171). Files is always on for every org (#80), so `false` always blocks rather than only mattering when a flag is set. */
|
|
@@ -149,6 +152,19 @@ export interface OrgPlatformReadiness {
|
|
|
149
152
|
* A missing key is then reported as "no reader given" instead of "not held".
|
|
150
153
|
*/
|
|
151
154
|
noKeyReader?: boolean;
|
|
155
|
+
/** The platform's declared git ref for each service, which an own org's services deploy. */
|
|
156
|
+
serviceGitRefs?: {
|
|
157
|
+
auth: string;
|
|
158
|
+
mail: string;
|
|
159
|
+
files: string;
|
|
160
|
+
jobs: string;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* The platform's recorded owner invite (`PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY`): the user id the
|
|
164
|
+
* platform's files host takes as `AUTHZ_BOOTSTRAP_OPERATOR`, or `'none'` when the platform has no
|
|
165
|
+
* owner to invite. `null` or absent until recorded. An own org's files host gets the same value.
|
|
166
|
+
*/
|
|
167
|
+
platformOwnerUserId?: string | null;
|
|
152
168
|
}
|
|
153
169
|
/**
|
|
154
170
|
* What an org's plan reads when no platform is declared: nothing is ready, so
|
|
@@ -205,6 +221,26 @@ heldRefs?: ReadonlySet<string>,
|
|
|
205
221
|
noKeyReader?: boolean,
|
|
206
222
|
/** #317: true when the host's executor was built with an `EdgePort` ({@link OrgPlatformReadiness.edgeReady}). */
|
|
207
223
|
edgePortPresent?: boolean): OrgPlatformReadiness;
|
|
224
|
+
/** The git refs the platform declares for the services an own org deploys. */
|
|
225
|
+
export declare function serviceGitRefsOf(platform: DeclaredPlatform): {
|
|
226
|
+
auth: string;
|
|
227
|
+
mail: string;
|
|
228
|
+
files: string;
|
|
229
|
+
jobs: string;
|
|
230
|
+
};
|
|
231
|
+
/** The runner app both the platform's and an own org's files step record. */
|
|
232
|
+
export declare const OWN_FILES_RUNNER_SCOPE_KEY = "coolify_platform_files_runner_app";
|
|
233
|
+
/**
|
|
234
|
+
* An own org (`services.own`) plans against its own services, so its readiness
|
|
235
|
+
* reads only its own recorded ids and its own held keys; nothing the shared
|
|
236
|
+
* services recorded reaches it. AI, edge and Access stay shared and come from
|
|
237
|
+
* `base`. Any other org gets `base` back unchanged.
|
|
238
|
+
*/
|
|
239
|
+
export declare function orgPlatformReadinessFor(org: DeclaredOrg, base: OrgPlatformReadiness, orgRecordedIds: Readonly<Record<string, string>>, heldRefs: ReadonlySet<string>): OrgPlatformReadiness;
|
|
240
|
+
/** The own org's held-key refs `heldKeys` resolves right now. A resolver that throws counts as not held, and its error goes nowhere. */
|
|
241
|
+
export declare function heldOwnKeyRefs(heldKeys: HeldKeyResolver, org: Pick<DeclaredOrg, 'slug'>): Promise<Set<string>>;
|
|
242
|
+
/** {@link agoraHostValuesFromPlatform}'s counterpart for an own org: every value is the org's own, null until recorded. */
|
|
243
|
+
export declare function agoraHostValuesForOwn(org: DeclaredOrg, orgRecordedIds: Readonly<Record<string, string>>): AgoraHostValues;
|
|
208
244
|
/**
|
|
209
245
|
* #177: {@link AgoraHostValues} derived from the same platform state, rather
|
|
210
246
|
* than requiring a bespoke host-supplied `AgoraHostPort` for values the
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { ownsServices } from '../org.js';
|
|
1
2
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
4
|
+
import { OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget } from './own-services.js';
|
|
5
|
+
import { JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
|
|
4
6
|
/**
|
|
5
7
|
* #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
|
|
6
8
|
* fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
|
|
@@ -27,6 +29,7 @@ export const NO_DOMAIN_MAIL_APEX = 'oikos.host';
|
|
|
27
29
|
export const DEFAULT_ORG_PLATFORM_READINESS = {
|
|
28
30
|
mailHost: null,
|
|
29
31
|
platformApex: null,
|
|
32
|
+
platformOwnerOrgId: null,
|
|
30
33
|
platformLettermintProjectId: null,
|
|
31
34
|
filesReady: false,
|
|
32
35
|
aiReady: false,
|
|
@@ -124,6 +127,8 @@ edgePortPresent = false) {
|
|
|
124
127
|
// mail.lettermintProjectId are facts regardless of whether mail itself is
|
|
125
128
|
// enabled or applied.
|
|
126
129
|
platformApex: platform.apex,
|
|
130
|
+
platformOwnerOrgId: platform.ownerOrgId || null,
|
|
131
|
+
platformOwnerUserId: platformRecordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY] || null,
|
|
127
132
|
platformLettermintProjectId: platform.mail.lettermintProjectId ?? null,
|
|
128
133
|
filesReady: filesApplied && missingKeys.files === null,
|
|
129
134
|
filesUrl: filesApplied
|
|
@@ -145,6 +150,95 @@ edgePortPresent = false) {
|
|
|
145
150
|
...(noKeyReader ? { noKeyReader } : {}),
|
|
146
151
|
};
|
|
147
152
|
}
|
|
153
|
+
/** The git refs the platform declares for the services an own org deploys. */
|
|
154
|
+
export function serviceGitRefsOf(platform) {
|
|
155
|
+
return {
|
|
156
|
+
auth: platform.auth.gitRef,
|
|
157
|
+
mail: platform.mail.gitRef,
|
|
158
|
+
files: platform.files.gitRef,
|
|
159
|
+
jobs: platform.jobs.gitRef,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** The runner app both the platform's and an own org's files step record. */
|
|
163
|
+
export const OWN_FILES_RUNNER_SCOPE_KEY = 'coolify_platform_files_runner_app';
|
|
164
|
+
const OWN_MAIL_APP_SCOPE_KEY = 'coolify_platform_mail_app';
|
|
165
|
+
/** Resource id of an own-service step, in `AppliedState.serviceIds`' `system:kind:key` form. */
|
|
166
|
+
function ownStepRecorded(ids, org, kind) {
|
|
167
|
+
return Boolean(ids[`coolify:${kind}:${org.id}`]);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The own org's sign-in is up: applied, so its ZITADEL answers. The broker and the login client
|
|
171
|
+
* come after the org's first broker entry, as the platform's do after its first registration, so
|
|
172
|
+
* waiting for them here would wait for the very steps this gate lets run.
|
|
173
|
+
*/
|
|
174
|
+
function ownAuthReady(ids) {
|
|
175
|
+
return Boolean(ids[OWN_SERVICES_APPLIED_SCOPE_KEY]);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* An own org (`services.own`) plans against its own services, so its readiness
|
|
179
|
+
* reads only its own recorded ids and its own held keys; nothing the shared
|
|
180
|
+
* services recorded reaches it. AI, edge and Access stay shared and come from
|
|
181
|
+
* `base`. Any other org gets `base` back unchanged.
|
|
182
|
+
*/
|
|
183
|
+
export function orgPlatformReadinessFor(org, base, orgRecordedIds, heldRefs) {
|
|
184
|
+
if (!ownsServices(org))
|
|
185
|
+
return base;
|
|
186
|
+
const refs = orgTarget(org).refs;
|
|
187
|
+
const authReady = ownAuthReady(orgRecordedIds);
|
|
188
|
+
const filesApplied = ownStepRecorded(orgRecordedIds, org, 'own-files') &&
|
|
189
|
+
Boolean(orgRecordedIds[OWN_FILES_RUNNER_SCOPE_KEY]);
|
|
190
|
+
const jobsApplied = ownStepRecorded(orgRecordedIds, org, 'own-jobs');
|
|
191
|
+
const mailApplied = ownStepRecorded(orgRecordedIds, org, 'own-mail') &&
|
|
192
|
+
Boolean(orgRecordedIds[OWN_MAIL_APP_SCOPE_KEY]);
|
|
193
|
+
const filesUrl = orgRecordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY];
|
|
194
|
+
const jobsUrl = orgRecordedIds[JOBS_INTERNAL_URL_SCOPE_KEY];
|
|
195
|
+
const missing = (applied, ref) => applied && !heldRefs.has(ref) ? ref : null;
|
|
196
|
+
const missingKeys = {
|
|
197
|
+
files: missing(filesApplied, refs.filesManagement),
|
|
198
|
+
ai: base.missingKeys.ai,
|
|
199
|
+
jobs: missing(jobsApplied, refs.jobsAdminToken),
|
|
200
|
+
};
|
|
201
|
+
const { jobsUrlMissing: _shared, ...rest } = base;
|
|
202
|
+
return {
|
|
203
|
+
...rest,
|
|
204
|
+
authReady,
|
|
205
|
+
authIssuer: authReady && org.domain ? `https://auth.${org.domain}` : null,
|
|
206
|
+
filesReady: filesApplied && filesUrl !== undefined && missingKeys.files === null,
|
|
207
|
+
filesUrl: filesApplied ? (filesUrl ?? null) : null,
|
|
208
|
+
jobsReady: jobsApplied && jobsUrl !== undefined && missingKeys.jobs === null,
|
|
209
|
+
...(jobsApplied && jobsUrl === undefined ? { jobsUrlMissing: true } : {}),
|
|
210
|
+
callsReady: ownStepRecorded(orgRecordedIds, org, 'own-sfu'),
|
|
211
|
+
mailHost: mailApplied && org.domain && base.mailHost
|
|
212
|
+
? { apex: org.domain, mailHost: base.mailHost.mailHost }
|
|
213
|
+
: null,
|
|
214
|
+
missingKeys,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** The own org's held-key refs `heldKeys` resolves right now. A resolver that throws counts as not held, and its error goes nowhere. */
|
|
218
|
+
export async function heldOwnKeyRefs(heldKeys, org) {
|
|
219
|
+
const held = new Set();
|
|
220
|
+
for (const ref of Object.values(orgTarget(org).refs)) {
|
|
221
|
+
try {
|
|
222
|
+
if (await heldKeys.resolve(ref))
|
|
223
|
+
held.add(ref);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// not held
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return held;
|
|
230
|
+
}
|
|
231
|
+
/** {@link agoraHostValuesFromPlatform}'s counterpart for an own org: every value is the org's own, null until recorded. */
|
|
232
|
+
export function agoraHostValuesForOwn(org, orgRecordedIds) {
|
|
233
|
+
const authReady = ownAuthReady(orgRecordedIds);
|
|
234
|
+
return {
|
|
235
|
+
issuer: authReady && org.domain ? `https://auth.${org.domain}` : null,
|
|
236
|
+
mailProjectId: orgRecordedIds[PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY] ?? null,
|
|
237
|
+
messagingProjectId: null,
|
|
238
|
+
mailUpstream: orgRecordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY] ?? null,
|
|
239
|
+
filesUpstream: orgRecordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY] ?? null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
148
242
|
/**
|
|
149
243
|
* #177: {@link AgoraHostValues} derived from the same platform state, rather
|
|
150
244
|
* than requiring a bespoke host-supplied `AgoraHostPort` for values the
|