@wtfalch/foundry 0.16.16 → 0.17.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/apply.d.ts +22 -1
- package/dist/bootstrap/bootstrap.d.ts +6 -1
- package/dist/bootstrap/bootstrap.js +26 -3
- package/dist/bootstrap/declare.d.ts +11 -1
- package/dist/bootstrap/declare.js +12 -2
- package/dist/index.d.ts +9 -4
- package/dist/index.js +7 -0
- package/dist/migrations/0038_calls_version_pin.sql +8 -0
- package/dist/migrations/0039_destroy_record.sql +7 -0
- package/dist/migrations/0040_company_included_uuid.sql +72 -0
- package/dist/migrations/0041_company_included_uuid_keys.sql +132 -0
- package/dist/migrations/0042_mutation_leases.sql +12 -0
- package/dist/org.d.ts +54 -0
- package/dist/org.js +43 -1
- package/dist/plan/adopt.d.ts +76 -0
- package/dist/plan/adopt.js +184 -0
- package/dist/plan/agora.d.ts +21 -0
- package/dist/plan/agora.js +37 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +13 -5
- package/dist/plan/chat-matrix.d.ts +1 -1
- package/dist/plan/chat-matrix.js +11 -3
- package/dist/plan/core-files.d.ts +25 -0
- package/dist/plan/core-files.js +25 -0
- package/dist/plan/diff.d.ts +11 -3
- package/dist/plan/diff.js +27 -62
- package/dist/plan/entry-ai-key.d.ts +3 -3
- package/dist/plan/entry-files-key.d.ts +3 -3
- package/dist/plan/executor.d.ts +57 -23
- package/dist/plan/executor.js +134 -50
- package/dist/plan/index.d.ts +4 -0
- package/dist/plan/index.js +2 -0
- package/dist/plan/live.d.ts +5 -0
- package/dist/plan/login-host.d.ts +1 -1
- package/dist/plan/login-host.js +6 -5
- package/dist/plan/plan-apply.d.ts +139 -6
- package/dist/plan/plan-apply.js +546 -76
- package/dist/plan/platform-diff.d.ts +14 -0
- package/dist/plan/platform-diff.js +16 -1
- package/dist/plan/platform-org.d.ts +16 -0
- package/dist/plan/platform-org.js +17 -1
- package/dist/plan/platform-providers.d.ts +1 -1
- package/dist/plan/platform-providers.js +73 -11
- package/dist/plan/providers-readers.d.ts +2 -0
- package/dist/plan/providers-readers.js +4 -0
- package/dist/plan/rollout.d.ts +52 -0
- package/dist/plan/rollout.js +37 -0
- package/dist/plan/step-system.d.ts +22 -0
- package/dist/plan/step-system.js +37 -0
- package/dist/plan.d.ts +3 -0
- package/dist/state.d.ts +13 -0
- package/dist/store/audit.js +15 -1
- package/dist/store/postgres.d.ts +1 -1
- package/dist/store/postgres.js +127 -14
- package/dist/store/schema.d.ts +134 -5
- package/dist/store/schema.js +12 -1
- package/dist/store.d.ts +47 -4
- package/dist/store.js +9 -1
- package/package.json +1 -1
package/dist/plan/executor.js
CHANGED
|
@@ -2,12 +2,13 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { URL } from 'node:url';
|
|
3
3
|
import { CALLS_SFU_PRIVATE_URL, CALLS_SFU_PUBLIC_URL } from '../calls-sfu.js';
|
|
4
4
|
import { APP_ENTRY_KEY, appEntry, filesCollectionEnvName, ownerMailboxRecorded, ownerMailboxes, sharedSecretRef, } from '../org.js';
|
|
5
|
-
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_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, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, desiredAgoraEnv, entryClientIdScopeKey, } from './agora.js';
|
|
5
|
+
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_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, desiredAgoraEnv, entryClientIdScopeKey, } from './agora.js';
|
|
6
6
|
import { BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_DATABASE_SCOPE_KEY, bouleEnvDrift, bouleHostnameFor, 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';
|
|
10
10
|
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
11
|
+
import { CORE_FILES_COLLECTION_NAME, FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY, } from './core-files.js';
|
|
11
12
|
import { AI_ENTRY_RENEW_WINDOW_DAYS, entryAiAppliedScopeKey, entryAiClampedScopeKey, entryAiExpiresScopeKey, entryAiPendingExpiresScopeKey, entryAiRetiredScopeKey, entryAiUndeliveredScopeKey, entryAiUnrevokedScopeKey, entryAiWebhookPendingScopeKey, entryAiWrittenScopeKey, parseIdList, } from './entry-ai-key.js';
|
|
12
13
|
import { FILES_AGORA_KEY_SCOPE_KEY, FILES_AGORA_UNDELIVERED_SCOPE_KEY, FILES_KEY_DAYS, FILES_KEY_RENEW_WINDOW_DAYS, FILES_ORG_OWNER_SCOPE_KEY, FILES_ROOT_EXPIRES_SCOPE_KEY, FILES_ROOT_PREVIOUS_SCOPE_KEY, FILES_ROOT_UNDELIVERED_SCOPE_KEY, FILES_ROOT_UNREVOKED_SCOPE_KEY, entryFilesAppliedScopeKey, entryFilesExpiresScopeKey, entryFilesMinterScopeKey, entryFilesPendingExpiresScopeKey, entryFilesRetiredScopeKey, entryFilesUndeliveredScopeKey, entryFilesUnrevokedScopeKey, entryFilesWrittenScopeKey, filesOwnerUserId, parseIdList as parseFilesIdList, } from './entry-files-key.js';
|
|
13
14
|
import { appHostname, coolifyDomains, entryHostnames, tunnelDnsHostnames, tunnelIngressHostnames, } from './hostnames.js';
|
|
@@ -210,14 +211,16 @@ const RATE_LIMIT_REQUESTS_PER_PERIOD = 10;
|
|
|
210
211
|
const DEFAULT_APP_REPO = 'wtfalch/foundry';
|
|
211
212
|
const DEFAULT_APP_DOCKERFILE = '/apps/hestia/Dockerfile';
|
|
212
213
|
/**
|
|
213
|
-
* The
|
|
214
|
-
* (https://github.com/wtfalch/cms) -- the
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* `dockerfile_location`. `CMS_APP_DOCKERFILE`
|
|
219
|
-
* `Dockerfile` (`apps/host`, single-org mode);
|
|
220
|
-
* is `apps/site`'s own Dockerfile (the reference
|
|
214
|
+
* The shared repo `cmsSite` builds its cms admin app from, always, and its
|
|
215
|
+
* site app from by default (https://github.com/wtfalch/cms) -- the
|
|
216
|
+
* 2026-09-25 decision this campaign implements
|
|
217
|
+
* (`.plans/2026-09-25-org-sites.md`, that repo): foundry never calls a cms
|
|
218
|
+
* admin API for the org's site, it provisions two ordinary Coolify
|
|
219
|
+
* applications, told apart by `dockerfile_location`. `CMS_APP_DOCKERFILE`
|
|
220
|
+
* is the repo's own root `Dockerfile` (`apps/host`, single-org mode);
|
|
221
|
+
* `CMS_SITE_APP_DOCKERFILE` is `apps/site`'s own Dockerfile (the reference
|
|
222
|
+
* renderer). An org with `site.repo` set (#245) builds its site app from
|
|
223
|
+
* that repo instead, the sample skin otherwise ships to every org.
|
|
221
224
|
*/
|
|
222
225
|
const CMS_REPO = 'wtfalch/cms';
|
|
223
226
|
const CMS_APP_DOCKERFILE = '/Dockerfile';
|
|
@@ -277,6 +280,8 @@ const LOG_DIR = '/app/logs';
|
|
|
277
280
|
*/
|
|
278
281
|
const CMS_APP_SCOPE_KEY = 'coolify_cms_app';
|
|
279
282
|
const SITE_APP_SCOPE_KEY = 'coolify_site_app';
|
|
283
|
+
/** #445: the one Coolify server every resource of this org lands on, chosen once and recorded. */
|
|
284
|
+
const COOLIFY_SERVER_SCOPE_KEY = 'coolify_server';
|
|
280
285
|
/**
|
|
281
286
|
* #117: the cms app's own Postgres database (`<org.slug>-cms-db`), found or
|
|
282
287
|
* created inside `cmsSite` the same way `coolifyDatabase` does for the org's
|
|
@@ -659,8 +664,6 @@ export class FoundryStepExecutor {
|
|
|
659
664
|
return action.key.includes(':')
|
|
660
665
|
? this.filesEntryKey(action, org, context)
|
|
661
666
|
: this.filesRootKey(action, org, context);
|
|
662
|
-
case 'files:storage':
|
|
663
|
-
return this.filesStorage(action, org);
|
|
664
667
|
case 'ai:organisation':
|
|
665
668
|
return this.aiOrganisation(action, org);
|
|
666
669
|
case 'ai:budget':
|
|
@@ -983,6 +986,7 @@ export class FoundryStepExecutor {
|
|
|
983
986
|
.map((key) => key.slice(ENTRY_CLIENT_ID_SCOPE_PREFIX.length)),
|
|
984
987
|
...(recorded[CMS_CLIENT_ID_SCOPE_KEY] ? ['cms'] : []),
|
|
985
988
|
...(recorded[AGORA_CLIENT_ID_SCOPE_KEY] ? ['agora'] : []),
|
|
989
|
+
...(recorded[BOULE_CLIENT_ID_SCOPE_KEY] ? ['boule'] : []),
|
|
986
990
|
].map((name) => recorded[`zitadel:app:${name}`]));
|
|
987
991
|
const projectId = recorded.zitadel_project;
|
|
988
992
|
// No project yet means no apps yet: vacuously safe to decide from (fix
|
|
@@ -1256,11 +1260,37 @@ export class FoundryStepExecutor {
|
|
|
1256
1260
|
context.recordScopeId('coolify_project', projectUuid);
|
|
1257
1261
|
return projectUuid;
|
|
1258
1262
|
}
|
|
1259
|
-
|
|
1263
|
+
/**
|
|
1264
|
+
* #445: one company lands on one Coolify server. The first call for an
|
|
1265
|
+
* org picks a usable, reachable server and records it
|
|
1266
|
+
* ({@link COOLIFY_SERVER_SCOPE_KEY}); every later call -- this apply, or a
|
|
1267
|
+
* later one -- reads that recording back and returns the same server,
|
|
1268
|
+
* regardless of what the pool's order or health looks like by then. A
|
|
1269
|
+
* recorded server that has gone unusable or unreachable is reported
|
|
1270
|
+
* blocked (never silently replaced with a second host, which would split
|
|
1271
|
+
* the org's resources across servers and break their internal container
|
|
1272
|
+
* URLs); a recorded server no longer registered at all is the same.
|
|
1273
|
+
* Deliberately no fallback to `servers[0]` when nothing is usable: an
|
|
1274
|
+
* unusable server picked blind is not a server anyone's resources could
|
|
1275
|
+
* actually be created on.
|
|
1276
|
+
*/
|
|
1277
|
+
async resolveServer(org, context) {
|
|
1260
1278
|
const servers = await this.ports.coolify.listServers();
|
|
1261
|
-
const
|
|
1279
|
+
const recorded = context.recordedIds()[COOLIFY_SERVER_SCOPE_KEY];
|
|
1280
|
+
if (recorded) {
|
|
1281
|
+
const server = servers.find((s) => s.uuid === recorded);
|
|
1282
|
+
if (!server) {
|
|
1283
|
+
throw new Error(`${org.slug}'s recorded Coolify server ${recorded} is no longer registered on this instance`);
|
|
1284
|
+
}
|
|
1285
|
+
if (!(server.is_usable && server.is_reachable)) {
|
|
1286
|
+
throw new Error(`${org.slug}'s recorded Coolify server ${recorded} is not usable/reachable right now; it is not moved to a second server`);
|
|
1287
|
+
}
|
|
1288
|
+
return server;
|
|
1289
|
+
}
|
|
1290
|
+
const server = servers.find((s) => s.is_usable && s.is_reachable);
|
|
1262
1291
|
if (!server)
|
|
1263
1292
|
throw new Error('no usable Coolify server is registered on this instance');
|
|
1293
|
+
context.recordScopeId(COOLIFY_SERVER_SCOPE_KEY, server.uuid);
|
|
1264
1294
|
return server;
|
|
1265
1295
|
}
|
|
1266
1296
|
async coolifyApp(action, org, context) {
|
|
@@ -1326,7 +1356,7 @@ export class FoundryStepExecutor {
|
|
|
1326
1356
|
haveNames = appliedAppDomains(org, context.recordedIds(), entry.key);
|
|
1327
1357
|
}
|
|
1328
1358
|
else {
|
|
1329
|
-
const server = await this.resolveServer();
|
|
1359
|
+
const server = await this.resolveServer(org, context);
|
|
1330
1360
|
const githubApp = await this.ports.coolify.requireGithubApp();
|
|
1331
1361
|
const created = await this.ports.coolify.createGithubAppApplication({
|
|
1332
1362
|
project_uuid: projectUuid,
|
|
@@ -1529,7 +1559,7 @@ export class FoundryStepExecutor {
|
|
|
1529
1559
|
dbId = found.uuid;
|
|
1530
1560
|
}
|
|
1531
1561
|
else {
|
|
1532
|
-
const server = await this.resolveServer();
|
|
1562
|
+
const server = await this.resolveServer(org, context);
|
|
1533
1563
|
const dbName = org.slug.replace(/-/g, '_');
|
|
1534
1564
|
const created = await this.ports.coolify.createPostgresDatabase({
|
|
1535
1565
|
project_uuid: projectUuid,
|
|
@@ -2161,16 +2191,22 @@ export class FoundryStepExecutor {
|
|
|
2161
2191
|
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
2162
2192
|
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
2163
2193
|
const site = await this.findOrCreateCmsApp({
|
|
2194
|
+
org,
|
|
2195
|
+
context,
|
|
2164
2196
|
projectUuid,
|
|
2165
2197
|
environment,
|
|
2166
2198
|
name: `${org.slug}-site`,
|
|
2199
|
+
repo: org.site.repo ?? CMS_REPO,
|
|
2167
2200
|
dockerfileLocation: CMS_SITE_APP_DOCKERFILE,
|
|
2168
2201
|
domain,
|
|
2169
2202
|
});
|
|
2170
2203
|
const cms = await this.findOrCreateCmsApp({
|
|
2204
|
+
org,
|
|
2205
|
+
context,
|
|
2171
2206
|
projectUuid,
|
|
2172
2207
|
environment,
|
|
2173
2208
|
name: `${org.slug}-cms`,
|
|
2209
|
+
repo: CMS_REPO,
|
|
2174
2210
|
dockerfileLocation: CMS_APP_DOCKERFILE,
|
|
2175
2211
|
domain: cmsHostnameFor(org),
|
|
2176
2212
|
});
|
|
@@ -2281,7 +2317,7 @@ export class FoundryStepExecutor {
|
|
|
2281
2317
|
const found = (await this.ports.coolify.listDatabases()).find((d) => d.name === name && d.environment_id === environment.id);
|
|
2282
2318
|
if (found)
|
|
2283
2319
|
return found.uuid;
|
|
2284
|
-
const server = await this.resolveServer();
|
|
2320
|
+
const server = await this.resolveServer(org, context);
|
|
2285
2321
|
const dbName = name.replace(/-/g, '_');
|
|
2286
2322
|
const created = await this.ports.coolify.createPostgresDatabase({
|
|
2287
2323
|
project_uuid: projectUuid,
|
|
@@ -2368,6 +2404,10 @@ export class FoundryStepExecutor {
|
|
|
2368
2404
|
* `coolifyApp`'s own analogous gap (it never restarted a found-but-stopped
|
|
2369
2405
|
* app either) has since closed the same way, inline in that method (#130);
|
|
2370
2406
|
* this method is `cmsSite`'s only caller.
|
|
2407
|
+
*
|
|
2408
|
+
* `repo` (#245) lets the site app build from an org's own skin repo
|
|
2409
|
+
* (`org.site.repo`) instead of the shared `CMS_REPO`; `cmsSite` always
|
|
2410
|
+
* passes `CMS_REPO` for the cms admin app, which is never org-specific.
|
|
2371
2411
|
*/
|
|
2372
2412
|
async findOrCreateCmsApp(input) {
|
|
2373
2413
|
const existing = await this.ports.coolify.findApplicationByName(input.name, {
|
|
@@ -2380,7 +2420,7 @@ export class FoundryStepExecutor {
|
|
|
2380
2420
|
}
|
|
2381
2421
|
return { appId: existing.uuid, existed: true };
|
|
2382
2422
|
}
|
|
2383
|
-
const server = await this.resolveServer();
|
|
2423
|
+
const server = await this.resolveServer(input.org, input.context);
|
|
2384
2424
|
const githubApp = await this.ports.coolify.requireGithubApp();
|
|
2385
2425
|
const created = await this.ports.coolify.createGithubAppApplication({
|
|
2386
2426
|
project_uuid: input.projectUuid,
|
|
@@ -2388,7 +2428,7 @@ export class FoundryStepExecutor {
|
|
|
2388
2428
|
environment_name: input.environment.name,
|
|
2389
2429
|
environment_uuid: input.environment.uuid,
|
|
2390
2430
|
github_app_uuid: githubApp.uuid,
|
|
2391
|
-
git_repository:
|
|
2431
|
+
git_repository: input.repo,
|
|
2392
2432
|
git_branch: 'main',
|
|
2393
2433
|
build_pack: 'dockerfile',
|
|
2394
2434
|
name: input.name,
|
|
@@ -2426,16 +2466,16 @@ export class FoundryStepExecutor {
|
|
|
2426
2466
|
}
|
|
2427
2467
|
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
2428
2468
|
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
2429
|
-
const server = await this.resolveServer();
|
|
2469
|
+
const server = await this.resolveServer(org, context);
|
|
2430
2470
|
const names = {
|
|
2431
2471
|
synapse: `${org.slug}-calls-synapse`,
|
|
2432
2472
|
mas: `${org.slug}-calls-mas`,
|
|
2433
2473
|
synapseDb: `${org.slug}-calls-synapse-db`,
|
|
2434
2474
|
masDb: `${org.slug}-calls-mas-db`,
|
|
2435
2475
|
};
|
|
2436
|
-
const synapseDbId = await this.ensureCallsDatabase(context, projectUuid, environment, names.synapseDb, 'synapse', CALLS_SYNAPSE_DB_SCOPE_KEY, CALLS_SYNAPSE_DB_INITDB_ARGS);
|
|
2476
|
+
const synapseDbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, names.synapseDb, 'synapse', CALLS_SYNAPSE_DB_SCOPE_KEY, CALLS_SYNAPSE_DB_INITDB_ARGS);
|
|
2437
2477
|
context.recordScopeId(CALLS_SYNAPSE_DB_SCOPE_KEY, synapseDbId);
|
|
2438
|
-
const masDbId = await this.ensureCallsDatabase(context, projectUuid, environment, names.masDb, 'mas', CALLS_MAS_DB_SCOPE_KEY);
|
|
2478
|
+
const masDbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, names.masDb, 'mas', CALLS_MAS_DB_SCOPE_KEY);
|
|
2439
2479
|
context.recordScopeId(CALLS_MAS_DB_SCOPE_KEY, masDbId);
|
|
2440
2480
|
const [synapseDbRow, masDbRow] = await Promise.all([
|
|
2441
2481
|
getDatabaseStarted(this.ports.coolify, synapseDbId),
|
|
@@ -2589,14 +2629,14 @@ export class FoundryStepExecutor {
|
|
|
2589
2629
|
* database, whose {@link CALLS_SERVICE_DB_SCOPE_KEY} has no
|
|
2590
2630
|
* `system:kind:key` slot of its own for the same reason.
|
|
2591
2631
|
*/
|
|
2592
|
-
async ensureCallsDatabase(context, projectUuid, environment, name, postgresDb, scopeKey, postgresInitdbArgs) {
|
|
2632
|
+
async ensureCallsDatabase(org, context, projectUuid, environment, name, postgresDb, scopeKey, postgresInitdbArgs) {
|
|
2593
2633
|
const existing = context.recordedIds()[scopeKey];
|
|
2594
2634
|
if (existing)
|
|
2595
2635
|
return existing;
|
|
2596
2636
|
const found = (await this.ports.coolify.listDatabases()).find((d) => d.name === name && d.environment_id === environment.id);
|
|
2597
2637
|
if (found)
|
|
2598
2638
|
return found.uuid;
|
|
2599
|
-
const server = await this.resolveServer();
|
|
2639
|
+
const server = await this.resolveServer(org, context);
|
|
2600
2640
|
const created = await this.ports.coolify.createPostgresDatabase({
|
|
2601
2641
|
project_uuid: projectUuid,
|
|
2602
2642
|
server_uuid: server.uuid,
|
|
@@ -2722,13 +2762,13 @@ export class FoundryStepExecutor {
|
|
|
2722
2762
|
await this.ensureChatAdmins(org, zitadelOrgId, project.id, recorded);
|
|
2723
2763
|
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
2724
2764
|
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
2725
|
-
const server = await this.resolveServer();
|
|
2765
|
+
const server = await this.resolveServer(org, context);
|
|
2726
2766
|
const names = chatInternalNames(org.slug);
|
|
2727
|
-
const synapseDbId = await this.ensureCallsDatabase(context, projectUuid, environment, names.synapseDb, 'synapse', CHAT_SYNAPSE_DB_SCOPE_KEY, CALLS_SYNAPSE_DB_INITDB_ARGS);
|
|
2767
|
+
const synapseDbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, names.synapseDb, 'synapse', CHAT_SYNAPSE_DB_SCOPE_KEY, CALLS_SYNAPSE_DB_INITDB_ARGS);
|
|
2728
2768
|
context.recordScopeId(CHAT_SYNAPSE_DB_SCOPE_KEY, synapseDbId);
|
|
2729
|
-
const masDbId = await this.ensureCallsDatabase(context, projectUuid, environment, names.masDb, 'mas', CHAT_MAS_DB_SCOPE_KEY);
|
|
2769
|
+
const masDbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, names.masDb, 'mas', CHAT_MAS_DB_SCOPE_KEY);
|
|
2730
2770
|
context.recordScopeId(CHAT_MAS_DB_SCOPE_KEY, masDbId);
|
|
2731
|
-
const serviceDbId = await this.ensureCallsDatabase(context, projectUuid, environment, names.serviceDb, 'chat', CHAT_SERVICE_DB_SCOPE_KEY);
|
|
2771
|
+
const serviceDbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, names.serviceDb, 'chat', CHAT_SERVICE_DB_SCOPE_KEY);
|
|
2732
2772
|
context.recordScopeId(CHAT_SERVICE_DB_SCOPE_KEY, serviceDbId);
|
|
2733
2773
|
const [synapseDbRow, masDbRow, serviceDbRow] = await Promise.all([
|
|
2734
2774
|
getDatabaseStarted(this.ports.coolify, synapseDbId),
|
|
@@ -2904,13 +2944,13 @@ export class FoundryStepExecutor {
|
|
|
2904
2944
|
const synapseHost = `${org.slug}-calls-synapse`;
|
|
2905
2945
|
const masHost = `${org.slug}-calls-mas`;
|
|
2906
2946
|
const dbName = `${name}-db`;
|
|
2907
|
-
const dbId = await this.ensureCallsDatabase(context, projectUuid, environment, dbName, dbName.replace(/-/g, '_'), CALLS_SERVICE_DB_SCOPE_KEY);
|
|
2947
|
+
const dbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, dbName, dbName.replace(/-/g, '_'), CALLS_SERVICE_DB_SCOPE_KEY);
|
|
2908
2948
|
context.recordScopeId(CALLS_SERVICE_DB_SCOPE_KEY, dbId);
|
|
2909
2949
|
const db = await getDatabaseStarted(this.ports.coolify, dbId);
|
|
2910
2950
|
if (!db.internal_db_url) {
|
|
2911
2951
|
throw new Error(`coolify:calls-service for ${org.slug}: its database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
2912
2952
|
}
|
|
2913
|
-
const server = await this.resolveServer();
|
|
2953
|
+
const server = await this.resolveServer(org, context);
|
|
2914
2954
|
const githubApp = await this.ports.coolify.requireGithubApp();
|
|
2915
2955
|
const app = await this.findOrCreateCallsApp(name, environment.id, () => this.ports.coolify.createGithubAppApplication({
|
|
2916
2956
|
project_uuid: projectUuid,
|
|
@@ -3123,6 +3163,19 @@ export class FoundryStepExecutor {
|
|
|
3123
3163
|
* pushed once calls are on and this org's calls service app carries its
|
|
3124
3164
|
* internal name (`desiredAgoraEnv` reads that marker from the recorded ids,
|
|
3125
3165
|
* not the host values).
|
|
3166
|
+
*
|
|
3167
|
+
* #442: a found-but-already-running app's container binds env only at
|
|
3168
|
+
* start, so writing it alone leaves that process on its old config --
|
|
3169
|
+
* restarted (never redeployed: no new image, so `restart` is the same
|
|
3170
|
+
* primitive `ensureLogVolume`'s own restart uses) whenever the live env
|
|
3171
|
+
* actually differs, or whenever the last restart this step confirmed
|
|
3172
|
+
* ({@link AGORA_ACTIVATED_ENV_SCOPE_KEY}) is not this exact env (an org
|
|
3173
|
+
* applied before this existed, whose live env already matched desired
|
|
3174
|
+
* with nothing ever confirmed running it). A brand new app has nothing
|
|
3175
|
+
* running yet to restart. The activation marker is recorded last, after
|
|
3176
|
+
* the restart (if any) returns without throwing, so a failed restart
|
|
3177
|
+
* leaves it stale and the next plan replans this step even though
|
|
3178
|
+
* Coolify's own env store already matches desired.
|
|
3126
3179
|
*/
|
|
3127
3180
|
async coolifyAgora(action, org, context) {
|
|
3128
3181
|
// The host values the plan read, carried in the step: never read again
|
|
@@ -3138,7 +3191,7 @@ export class FoundryStepExecutor {
|
|
|
3138
3191
|
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
3139
3192
|
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
3140
3193
|
const name = `${org.slug}-agora`;
|
|
3141
|
-
const server = await this.resolveServer();
|
|
3194
|
+
const server = await this.resolveServer(org, context);
|
|
3142
3195
|
const githubApp = await this.ports.coolify.requireGithubApp();
|
|
3143
3196
|
const app = await this.findOrCreateCallsApp(name, environment.id, () => this.ports.coolify.createGithubAppApplication({
|
|
3144
3197
|
project_uuid: projectUuid,
|
|
@@ -3155,7 +3208,20 @@ export class FoundryStepExecutor {
|
|
|
3155
3208
|
}));
|
|
3156
3209
|
// Every app foundry deploys keeps its own logs (#75), reused as-is here.
|
|
3157
3210
|
await this.ensureLogVolume(app.appId, org, app.existed);
|
|
3211
|
+
// #442: read before overwriting -- `bulkUpsertEnvs` below always leaves
|
|
3212
|
+
// Coolify's own env store matching `env`, which is why that store alone
|
|
3213
|
+
// can no longer tell a changed value from one already live.
|
|
3214
|
+
const liveEnv = new Map((await this.ports.coolify.listEnvs(app.appId))
|
|
3215
|
+
.filter((e) => !e.is_preview)
|
|
3216
|
+
.map((e) => [e.key, e.value]));
|
|
3217
|
+
const envChanged = agoraEnvDrift(env, Object.fromEntries(liveEnv)).length > 0;
|
|
3158
3218
|
await this.ports.coolify.bulkUpsertEnvs(app.appId, Object.entries(env).map(([key, value]) => ({ key, value })));
|
|
3219
|
+
const revision = agoraEnvRevision(env);
|
|
3220
|
+
const alreadyActivated = context.recordedIds()[AGORA_ACTIVATED_ENV_SCOPE_KEY] === revision;
|
|
3221
|
+
if (app.existed && (envChanged || !alreadyActivated)) {
|
|
3222
|
+
await this.ports.coolify.restart(app.appId);
|
|
3223
|
+
}
|
|
3224
|
+
context.recordScopeId(AGORA_ACTIVATED_ENV_SCOPE_KEY, revision);
|
|
3159
3225
|
return { resourceId: app.appId };
|
|
3160
3226
|
}
|
|
3161
3227
|
// ---- boule (#161) --------------------------------------------------------
|
|
@@ -3195,9 +3261,9 @@ export class FoundryStepExecutor {
|
|
|
3195
3261
|
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
3196
3262
|
const name = `${org.slug}-boule`;
|
|
3197
3263
|
const dbName = `${org.slug}-boule-db`.replace(/-/g, '_');
|
|
3198
|
-
const dbId = await this.ensureCallsDatabase(context, projectUuid, environment, `${org.slug}-boule-db`, dbName, BOULE_DATABASE_SCOPE_KEY);
|
|
3264
|
+
const dbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, `${org.slug}-boule-db`, dbName, BOULE_DATABASE_SCOPE_KEY);
|
|
3199
3265
|
context.recordScopeId(BOULE_DATABASE_SCOPE_KEY, dbId);
|
|
3200
|
-
const server = await this.resolveServer();
|
|
3266
|
+
const server = await this.resolveServer(org, context);
|
|
3201
3267
|
const githubApp = await this.ports.coolify.requireGithubApp();
|
|
3202
3268
|
const app = await this.findOrCreateCallsApp(name, environment.id, () => this.ports.coolify.createGithubAppApplication({
|
|
3203
3269
|
project_uuid: projectUuid,
|
|
@@ -3409,7 +3475,7 @@ export class FoundryStepExecutor {
|
|
|
3409
3475
|
appName: `${org.slug}-cloudflared`,
|
|
3410
3476
|
projectUuid,
|
|
3411
3477
|
environment,
|
|
3412
|
-
resolveServer: () => this.resolveServer(),
|
|
3478
|
+
resolveServer: () => this.resolveServer(org, context),
|
|
3413
3479
|
recordedConnectorAppId: context.recordedIds()[CLOUDFLARE_TUNNEL_CONNECTOR_APP_SCOPE_KEY] ?? null,
|
|
3414
3480
|
recordAppId: async (appId) => {
|
|
3415
3481
|
context.recordScopeId(CLOUDFLARE_TUNNEL_CONNECTOR_APP_SCOPE_KEY, appId);
|
|
@@ -4029,11 +4095,11 @@ export class FoundryStepExecutor {
|
|
|
4029
4095
|
}
|
|
4030
4096
|
// ---- files / ai --------------------------------------------------------
|
|
4031
4097
|
/**
|
|
4032
|
-
* #80: provisions the org's own organisation on Files
|
|
4033
|
-
*
|
|
4034
|
-
*
|
|
4035
|
-
*
|
|
4036
|
-
*
|
|
4098
|
+
* #80: provisions the org's own organisation on Files.
|
|
4099
|
+
* `provisionOrganisation` is safely repeatable on its own
|
|
4100
|
+
* (`FilesOrganisationPort`'s doc comment), so this is called every time
|
|
4101
|
+
* regardless of whether it already ran -- idempotent by find-or-create,
|
|
4102
|
+
* not by checking `recordedIds` first.
|
|
4037
4103
|
*
|
|
4038
4104
|
* F6b: `action.desired.shareHost` (`diff.ts`'s `planFilesOrganisation`) is
|
|
4039
4105
|
* passed through every time, even though `entry-files-key.ts` reports the
|
|
@@ -4046,15 +4112,23 @@ export class FoundryStepExecutor {
|
|
|
4046
4112
|
* repeat-is-a-no-op shape as `provisionOrganisation` itself -- every org
|
|
4047
4113
|
* gets an Agora (#164), so `diff.ts`'s `planFilesOrganisation` always sets
|
|
4048
4114
|
* `desired.agoraOrigin`.
|
|
4115
|
+
*
|
|
4116
|
+
* #437: the owner member/name, the core ZITADEL files project and the core
|
|
4117
|
+
* collection (`core-files.ts`) are no longer conditioned on a hosting entry
|
|
4118
|
+
* declaring `uses.files` -- the core bundle (Agora's own uploads) needs
|
|
4119
|
+
* all three just as much as a bespoke entry does, and withholding them
|
|
4120
|
+
* until one existed is why a plain Agora/Boule org never got a usable
|
|
4121
|
+
* Files setup at all. Both the project and the collection are idempotent
|
|
4122
|
+
* by name, so a re-run that already created either finds it instead.
|
|
4049
4123
|
*/
|
|
4050
4124
|
async filesOrganisation(action, org, context) {
|
|
4051
4125
|
const files = await this.filesPort();
|
|
4052
|
-
|
|
4053
|
-
// organisation
|
|
4054
|
-
//
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4126
|
+
const recorded = context.recordedIds();
|
|
4127
|
+
// F6b: an organisation takes an owner member and name only while it is
|
|
4128
|
+
// being created, so it is passed whenever the owner is known now --
|
|
4129
|
+
// every org, not only one with a hosting entry declaring `uses.files`
|
|
4130
|
+
// (#437).
|
|
4131
|
+
const owner = filesOwnerUserId(org, recorded);
|
|
4058
4132
|
const provisioned = await files.provisionOrganisation({
|
|
4059
4133
|
organisationId: org.id,
|
|
4060
4134
|
ceilingBytes: action.desired.ceilingBytes ?? null,
|
|
@@ -4071,6 +4145,22 @@ export class FoundryStepExecutor {
|
|
|
4071
4145
|
if (provisioned.agoraKey) {
|
|
4072
4146
|
await this.deliverFilesAgoraKey(files, org, provisioned.agoraKey, context);
|
|
4073
4147
|
}
|
|
4148
|
+
// #437: the audience Agora requests to read/browse Files
|
|
4149
|
+
// (`agoras.ts`'s `filesProjectId`), the same `urn:zitadel:iam:org:
|
|
4150
|
+
// project:id:<id>:aud` shape `messagingProjectId` already uses. Retried
|
|
4151
|
+
// on a later apply, same as the owner above, while the org's ZITADEL
|
|
4152
|
+
// org id is not recorded yet (`zitadel:issuer` has not run).
|
|
4153
|
+
const zitadelOrgId = recorded[ZITADEL_ORG_SCOPE_KEY];
|
|
4154
|
+
if (zitadelOrgId) {
|
|
4155
|
+
const { project } = await this.ports.zitadel.findOrCreateProject(zitadelOrgId, `${org.slug}-files`);
|
|
4156
|
+
context.recordScopeId(ZITADEL_FILES_PROJECT_SCOPE_KEY, project.id);
|
|
4157
|
+
}
|
|
4158
|
+
// #437: the one collection every file Agora itself uploads goes into
|
|
4159
|
+
// (`agoras.ts`'s `filesCollectionId`) -- Files has no route to list an
|
|
4160
|
+
// organisation's collections and an ordinary member cannot create one,
|
|
4161
|
+
// so it has to exist before Agora ever tries.
|
|
4162
|
+
const collection = await files.ensureCollection(org.id, CORE_FILES_COLLECTION_NAME);
|
|
4163
|
+
context.recordScopeId(FILES_CORE_COLLECTION_SCOPE_KEY, collection.id);
|
|
4074
4164
|
return { resourceId: provisioned.organisationId };
|
|
4075
4165
|
}
|
|
4076
4166
|
/**
|
|
@@ -4107,12 +4197,6 @@ export class FoundryStepExecutor {
|
|
|
4107
4197
|
context.recordScopeId(FILES_AGORA_UNDELIVERED_SCOPE_KEY, '');
|
|
4108
4198
|
throw new Error(`${at}: Agora's key ${key.id} could not be delivered after ${AI_KEY_DELIVERY_ATTEMPTS} attempts (${cause}); ${revoked ? 'it was revoked again' : 'revoke it in files'}`);
|
|
4109
4199
|
}
|
|
4110
|
-
async filesStorage(action, org) {
|
|
4111
|
-
const files = await this.filesPort();
|
|
4112
|
-
await files.setQuota(org.id, action.desired.limitBytes ?? null);
|
|
4113
|
-
await files.setBudget(org.id, action.desired.limitMicros ?? null);
|
|
4114
|
-
return { resourceId: org.id };
|
|
4115
|
-
}
|
|
4116
4200
|
/**
|
|
4117
4201
|
* #80: provisions the org's own organisation on AI and switches its AI use
|
|
4118
4202
|
* on, before {@link aiBudget} can set its own spend limit. Both
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -2,12 +2,16 @@ export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, File
|
|
|
2
2
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
3
3
|
export type { StepAction, PlannedStep, PlanDetail } from './diff.js';
|
|
4
4
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
5
|
+
export type { StepSystem } from './step-system.js';
|
|
6
|
+
export { stepSystemOf } from './step-system.js';
|
|
5
7
|
export type { PlatformPlanDetail } from './platform-diff.js';
|
|
6
8
|
export { planPlatform, toPlatformPlan, 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, } from './platform-diff.js';
|
|
7
9
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
|
|
8
10
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
9
11
|
export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
|
10
12
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
13
|
+
export type { AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot } from './adopt.js';
|
|
14
|
+
export { AdoptRefusedError } from './adopt.js';
|
|
11
15
|
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan-apply.js';
|
|
12
16
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
13
17
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
2
2
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
3
|
+
export { stepSystemOf } from './step-system.js';
|
|
3
4
|
export { planPlatform, toPlatformPlan, 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, } from './platform-diff.js';
|
|
4
5
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
5
6
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
7
|
+
export { AdoptRefusedError } from './adopt.js';
|
|
6
8
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
7
9
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
8
10
|
// #171: how a host signs files management requests with the delivered key.
|
package/dist/plan/live.d.ts
CHANGED
|
@@ -48,6 +48,11 @@ export interface ZitadelReader {
|
|
|
48
48
|
}
|
|
49
49
|
export interface CoolifyReader {
|
|
50
50
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Reads one Coolify project by its uuid, for an org that records none yet
|
|
53
|
+
* (adopt, archon#271). Optional: without it nothing can be adopted.
|
|
54
|
+
*/
|
|
55
|
+
listProject?(projectUuid: string): Promise<LiveResource[]>;
|
|
51
56
|
}
|
|
52
57
|
export interface CloudflareReader {
|
|
53
58
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
@@ -66,7 +66,7 @@ export interface LoginHostBindingsPort {
|
|
|
66
66
|
* on the next run.
|
|
67
67
|
*/
|
|
68
68
|
export declare const LOGIN_BINDING_SCOPE_KEY = "login_host_binding";
|
|
69
|
-
/** The org's recorded app client ids (hosted app, each entry's own, cms, agora), sorted. */
|
|
69
|
+
/** The org's recorded app client ids (hosted app, each entry's own, cms, agora, boule), deduplicated and sorted. */
|
|
70
70
|
export declare function loginHostClientIds(recorded: Readonly<Record<string, string>>): string[];
|
|
71
71
|
/** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
|
|
72
72
|
export declare function loginBindingState(host: string, clientIds: readonly string[]): string;
|
package/dist/plan/login-host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, } from './agora.js';
|
|
2
|
+
import { BOULE_CLIENT_ID_SCOPE_KEY } from './boule.js';
|
|
2
3
|
/**
|
|
3
4
|
* #267: what both the planner (`diff.ts`) and the executor (`executor.ts`)
|
|
4
5
|
* need to agree on for an org's own company login host -- the hostname and
|
|
@@ -36,18 +37,18 @@ export function loginHostnameFor(org) {
|
|
|
36
37
|
* on the next run.
|
|
37
38
|
*/
|
|
38
39
|
export const LOGIN_BINDING_SCOPE_KEY = 'login_host_binding';
|
|
39
|
-
/** The org's recorded app client ids (hosted app, each entry's own, cms, agora), sorted. */
|
|
40
|
+
/** The org's recorded app client ids (hosted app, each entry's own, cms, agora, boule), deduplicated and sorted. */
|
|
40
41
|
export function loginHostClientIds(recorded) {
|
|
41
|
-
|
|
42
|
+
const ids = [
|
|
42
43
|
recorded[APP_CLIENT_ID_SCOPE_KEY],
|
|
43
44
|
recorded[CMS_CLIENT_ID_SCOPE_KEY],
|
|
44
45
|
recorded[AGORA_CLIENT_ID_SCOPE_KEY],
|
|
46
|
+
recorded[BOULE_CLIENT_ID_SCOPE_KEY],
|
|
45
47
|
...Object.entries(recorded)
|
|
46
48
|
.filter(([key]) => key.startsWith(ENTRY_CLIENT_ID_SCOPE_PREFIX))
|
|
47
49
|
.map(([, id]) => id),
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
.sort();
|
|
50
|
+
].filter((id) => Boolean(id));
|
|
51
|
+
return [...new Set(ids)].sort();
|
|
51
52
|
}
|
|
52
53
|
/** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
|
|
53
54
|
export function loginBindingState(host, clientIds) {
|