@wtfalch/foundry 0.45.1 → 0.47.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/build-json.d.ts +28 -0
- package/dist/build-json.js +95 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +6 -2
- package/dist/migrations/0058_app_requests.sql +32 -0
- package/dist/org.d.ts +68 -5
- package/dist/org.js +287 -135
- package/dist/plan/agora.d.ts +14 -0
- package/dist/plan/agora.js +21 -0
- package/dist/plan/backup-gate.d.ts +15 -0
- package/dist/plan/backup-gate.js +38 -0
- package/dist/plan/boule-owners-http.d.ts +12 -0
- package/dist/plan/boule-owners-http.js +59 -0
- package/dist/plan/boule.d.ts +38 -2
- package/dist/plan/boule.js +72 -1
- package/dist/plan/company-backups.d.ts +7 -1
- package/dist/plan/company-backups.js +42 -2
- package/dist/plan/cookie-domain.d.ts +41 -0
- package/dist/plan/cookie-domain.js +71 -0
- package/dist/plan/desktop-auth.d.ts +58 -0
- package/dist/plan/desktop-auth.js +144 -0
- package/dist/plan/desktop-callback.d.ts +26 -0
- package/dist/plan/desktop-callback.js +59 -0
- package/dist/plan/diff.d.ts +13 -4
- package/dist/plan/diff.js +277 -88
- package/dist/plan/executor.d.ts +136 -6
- package/dist/plan/executor.js +732 -105
- package/dist/plan/health-gaps.js +5 -0
- package/dist/plan/health-targets.js +1 -2
- package/dist/plan/hostnames.d.ts +2 -2
- package/dist/plan/hostnames.js +6 -6
- package/dist/plan/index.d.ts +4 -1
- package/dist/plan/index.js +3 -1
- package/dist/plan/job-app.d.ts +23 -0
- package/dist/plan/job-app.js +41 -0
- package/dist/plan/live.d.ts +6 -0
- package/dist/plan/live.js +16 -0
- package/dist/plan/login-host.d.ts +28 -0
- package/dist/plan/login-host.js +36 -0
- package/dist/plan/org-names.js +9 -1
- package/dist/plan/org-postgres-job.d.ts +22 -0
- package/dist/plan/org-postgres-job.js +91 -0
- package/dist/plan/org-postgres-migrate.d.ts +115 -0
- package/dist/plan/org-postgres-migrate.js +466 -0
- package/dist/plan/org-postgres.d.ts +90 -0
- package/dist/plan/org-postgres.js +293 -0
- package/dist/plan/plan-apply.d.ts +60 -3
- package/dist/plan/plan-apply.js +174 -8
- package/dist/plan/platform-diff.d.ts +45 -0
- package/dist/plan/platform-diff.js +159 -4
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-org.js +4 -0
- package/dist/plan/platform-providers.d.ts +12 -2
- package/dist/plan/platform-providers.js +282 -9
- package/dist/plan/post-logout-uris.d.ts +22 -0
- package/dist/plan/post-logout-uris.js +43 -0
- package/dist/plan/providers-readers.d.ts +10 -1
- package/dist/plan/providers-readers.js +56 -5
- package/dist/plan/sign-in-bindings.d.ts +4 -0
- package/dist/plan/sign-in-bindings.js +10 -0
- package/dist/plan/sign-in-org.d.ts +19 -6
- package/dist/plan/sign-in-org.js +35 -10
- package/dist/plan/sign-in.d.ts +12 -5
- package/dist/plan/sign-in.js +12 -5
- package/dist/plan/signin-db-move.js +10 -34
- package/dist/plan/worker-app.js +3 -1
- package/dist/refuse-org.d.ts +7 -0
- package/dist/refuse-org.js +10 -0
- package/dist/requested-changes.d.ts +50 -4
- package/dist/requested-changes.js +9 -1
- package/dist/store/postgres.d.ts +5 -1
- package/dist/store/postgres.js +67 -8
- package/dist/store/schema.d.ts +72 -0
- package/dist/store/schema.js +4 -0
- package/dist/store.d.ts +24 -1
- package/dist/worker-app-config.d.ts +8 -2
- package/dist/worker-app-config.js +20 -6
- package/package.json +5 -5
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
3
3
|
import { sfuSlotPorts } from '../calls-sfu.js';
|
|
4
|
-
import { heldEnvRef, ownsServices, validateHosting } from '../org.js';
|
|
4
|
+
import { ORG_SERVICE_SCHEMAS, heldEnvRef, ownsServices, validateHosting } from '../org.js';
|
|
5
5
|
import { PLATFORM_SERVICE_KEYS } from '../platform.js';
|
|
6
6
|
import { AlreadyRunningError, StepFailedError } from '../runner/errors.js';
|
|
7
7
|
import { StaleLeaseError, } from '../store.js';
|
|
8
|
+
import { entryHasDatabase } from '../worker-app-config.js';
|
|
8
9
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
9
10
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
10
|
-
import { NetworkMoveBackupMissingError, SigninDbMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupForSigninMove, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
11
|
+
import { NetworkMoveBackupMissingError, OrgPostgresMigrationBackupMissingError, SigninDbMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupForOrgPostgresMigration, requireFreshBackupForSigninMove, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
11
12
|
import { ARCHON_REPO, PRIMARY_SERVER_SCOPE_KEY, archonSubject, entrySubject, } from './blue-green.js';
|
|
12
13
|
import { readLiveBuilds } from './build-record.js';
|
|
14
|
+
import { LEGACY_BACKUP_NAMES } from './company-backups.js';
|
|
13
15
|
import { unconfirmedUpgrades } from './confirm-upgrade.js';
|
|
14
16
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
15
17
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
@@ -20,6 +22,7 @@ import { hostingNames } from './locked-hostnames.js';
|
|
|
20
22
|
import { agoraIssuerHostFor, loginHostServesAgora, readLoginHostDiscovery, } from './login-host.js';
|
|
21
23
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMTP_PORT_MAX, OWN_MAIL_SMTP_PORT_SCOPE_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, isOwnMailSmtpPort, mailRelayFingerprint, } from './mail-relay.js';
|
|
22
24
|
import { findNameClashes, nameClashMessage } from './org-names.js';
|
|
25
|
+
import { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationNotConfirmedError, invalidMigrationConfirmation, migrateHomeOf, parseMigrationConfirmation, } from './org-postgres-migrate.js';
|
|
23
26
|
import { ownHostnames } from './own-hostnames.js';
|
|
24
27
|
import { OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, orgTarget } from './own-services.js';
|
|
25
28
|
import { PLATFORM_ARCHON_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
@@ -159,8 +162,11 @@ export class FoundryPlanApply {
|
|
|
159
162
|
agora;
|
|
160
163
|
jobsScheduler;
|
|
161
164
|
loginHostBindings;
|
|
165
|
+
loginHostClientOrganizations;
|
|
162
166
|
loginHostDiscovery;
|
|
163
167
|
zitadelIdTokenUserinfo;
|
|
168
|
+
zitadelPostLogoutUris;
|
|
169
|
+
zitadelRedirectUris;
|
|
164
170
|
zitadelMcp;
|
|
165
171
|
zitadelOrgMetadata;
|
|
166
172
|
broker;
|
|
@@ -194,8 +200,11 @@ export class FoundryPlanApply {
|
|
|
194
200
|
this.agora = options.agora;
|
|
195
201
|
this.jobsScheduler = options.jobsScheduler ?? false;
|
|
196
202
|
this.loginHostBindings = options.loginHostBindings ?? false;
|
|
203
|
+
this.loginHostClientOrganizations = options.loginHostClientOrganizations ?? false;
|
|
197
204
|
this.loginHostDiscovery = options.loginHostDiscovery;
|
|
198
205
|
this.zitadelIdTokenUserinfo = options.zitadelIdTokenUserinfo ?? false;
|
|
206
|
+
this.zitadelPostLogoutUris = options.zitadelPostLogoutUris ?? false;
|
|
207
|
+
this.zitadelRedirectUris = options.zitadelRedirectUris ?? false;
|
|
199
208
|
this.zitadelMcp = options.zitadelMcp ?? false;
|
|
200
209
|
this.zitadelOrgMetadata = options.zitadelOrgMetadata ?? false;
|
|
201
210
|
this.broker = options.broker ?? false;
|
|
@@ -379,6 +388,55 @@ export class FoundryPlanApply {
|
|
|
379
388
|
return released;
|
|
380
389
|
}));
|
|
381
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Records that the staging copy of a live company's data was checked, which is the only thing that lets
|
|
393
|
+
* the org Postgres migration step run (#444). It is its own call, never part of an apply. Needs the
|
|
394
|
+
* platform permission. It stores the staging copy id, who confirmed and when, and writes an audit row.
|
|
395
|
+
* Throws {@link InvalidMigrationConfirmationError} for a confirmation that is not valid.
|
|
396
|
+
*/
|
|
397
|
+
async confirmOrgPostgresMigration(orgId, confirmation) {
|
|
398
|
+
const record = (outcome, reason) => this.audit.recordAdoption({
|
|
399
|
+
actor: this.actor,
|
|
400
|
+
orgId,
|
|
401
|
+
action: 'confirm-org-postgres-migration',
|
|
402
|
+
entryKey: 'org_postgres_migration',
|
|
403
|
+
outcome,
|
|
404
|
+
...(reason === undefined ? {} : { reason }),
|
|
405
|
+
at: this.now(),
|
|
406
|
+
});
|
|
407
|
+
if ((await this.authz.canApplyPlatform?.(this.actor)) !== true) {
|
|
408
|
+
await record('refused', 'not authorized');
|
|
409
|
+
throw new NotAuthorizedError(this.actor, orgId);
|
|
410
|
+
}
|
|
411
|
+
const invalid = invalidMigrationConfirmation(confirmation);
|
|
412
|
+
if (invalid) {
|
|
413
|
+
await record('refused', invalid);
|
|
414
|
+
throw new InvalidMigrationConfirmationError(invalid);
|
|
415
|
+
}
|
|
416
|
+
const value = {
|
|
417
|
+
stagingCopy: confirmation.stagingCopy,
|
|
418
|
+
confirmedBy: confirmation.confirmedBy,
|
|
419
|
+
confirmedAt: confirmation.confirmedAt,
|
|
420
|
+
};
|
|
421
|
+
await this.withMutationLease(companyScope(orgId), (reason) => record('refused', reason), (fence) => this.withRunnerSerialization(orgId, (reason) => record('refused', reason), async () => {
|
|
422
|
+
if (!(await this.store.get(orgId))) {
|
|
423
|
+
await record('refused', `no declared org ${orgId}`);
|
|
424
|
+
throw new DeclaredOrgNotFoundError(orgId);
|
|
425
|
+
}
|
|
426
|
+
const previous = await this.store.getAppliedState(orgId);
|
|
427
|
+
await this.store.putAppliedState({
|
|
428
|
+
orgId,
|
|
429
|
+
serviceIds: {
|
|
430
|
+
...(previous?.serviceIds ?? {}),
|
|
431
|
+
[ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY]: JSON.stringify(value),
|
|
432
|
+
},
|
|
433
|
+
checkpoints: previous?.checkpoints ?? [],
|
|
434
|
+
failedFields: previous?.failedFields ?? [],
|
|
435
|
+
lastApplied: previous?.lastApplied ?? null,
|
|
436
|
+
}, fence);
|
|
437
|
+
await record('recorded');
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
382
440
|
async computeAdopt(id, input) {
|
|
383
441
|
const org = await this.store.get(id);
|
|
384
442
|
if (!org)
|
|
@@ -485,8 +543,43 @@ export class FoundryPlanApply {
|
|
|
485
543
|
throw e;
|
|
486
544
|
}
|
|
487
545
|
}
|
|
546
|
+
// A migration to the company's Postgres server needs the recorded staging confirmation and a fresh dump
|
|
547
|
+
// of the old database. Both are checked before the first port call.
|
|
548
|
+
const migrations = detail.plannedSteps.filter((s) => s.action.system === 'coolify' && s.action.kind === 'org-postgres-migrate');
|
|
549
|
+
if (migrations.length > 0) {
|
|
550
|
+
const ids = (await this.store.getAppliedState(orgId))?.serviceIds ?? {};
|
|
551
|
+
if (!parseMigrationConfirmation(ids[ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY])) {
|
|
552
|
+
await this.refuse(plan, 'org postgres migration without a recorded staging confirmation');
|
|
553
|
+
throw new OrgPostgresMigrationNotConfirmedError();
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
for (const migration of migrations) {
|
|
557
|
+
const home = ORG_SERVICE_SCHEMAS.find((h) => h.schema === migrateHomeOf(migration.action));
|
|
558
|
+
const legacyKey = home?.legacyKey(org);
|
|
559
|
+
const backupName = home ? LEGACY_BACKUP_NAMES[home.schema] : undefined;
|
|
560
|
+
if (!this.backupGate)
|
|
561
|
+
throw new OrgPostgresMigrationBackupMissingError('no-gate');
|
|
562
|
+
if (!legacyKey || !backupName) {
|
|
563
|
+
throw new OrgPostgresMigrationBackupMissingError('no-backup');
|
|
564
|
+
}
|
|
565
|
+
await requireFreshBackupForOrgPostgresMigration({
|
|
566
|
+
coolify: this.backupGate.coolify,
|
|
567
|
+
orgId,
|
|
568
|
+
recordedIds: ids,
|
|
569
|
+
now: () => this.now().getTime(),
|
|
570
|
+
}, legacyKey, backupName);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (e) {
|
|
574
|
+
if (e instanceof OrgPostgresMigrationBackupMissingError) {
|
|
575
|
+
await this.refuse(plan, 'org postgres migration without a fresh backup');
|
|
576
|
+
}
|
|
577
|
+
throw e;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
488
580
|
if (planNeedsConfirmation(plan.steps) &&
|
|
489
|
-
confirmedOrgName.trim() !== org.displayName.trim()
|
|
581
|
+
confirmedOrgName.trim() !== org.displayName.trim() &&
|
|
582
|
+
!entryConfirms(options.entryConfirmation, detail.plannedSteps, org.id, org.hosting.entries)) {
|
|
490
583
|
await this.refuse(plan, "destructive or migrating step without the org's name typed correctly");
|
|
491
584
|
throw new ConfirmationRequiredError(orgId);
|
|
492
585
|
}
|
|
@@ -1219,8 +1312,11 @@ export class FoundryPlanApply {
|
|
|
1219
1312
|
: undefined;
|
|
1220
1313
|
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, {
|
|
1221
1314
|
loginHostBindings: this.loginHostBindings,
|
|
1315
|
+
loginHostClientOrganizations: this.loginHostClientOrganizations,
|
|
1222
1316
|
loginHostServesDiscovery,
|
|
1223
1317
|
zitadelIdTokenUserinfo: this.zitadelIdTokenUserinfo,
|
|
1318
|
+
zitadelPostLogoutUris: this.zitadelPostLogoutUris,
|
|
1319
|
+
zitadelRedirectUris: this.zitadelRedirectUris,
|
|
1224
1320
|
zitadelMcp: this.zitadelMcp,
|
|
1225
1321
|
zitadelOrgMetadata: this.zitadelOrgMetadata,
|
|
1226
1322
|
broker: ownsServices(org) && this.hostOwnServices && this.executor.ownBrokerPort
|
|
@@ -1356,14 +1452,16 @@ export class FoundryPlanApply {
|
|
|
1356
1452
|
...(await this.sharedPlatformReadiness()),
|
|
1357
1453
|
heldEnv: await this.heldEnvReadiness(org),
|
|
1358
1454
|
};
|
|
1359
|
-
if (!ownsServices(org))
|
|
1360
|
-
return base;
|
|
1361
|
-
const held = this.platformDeps?.heldKeys;
|
|
1362
1455
|
const platform = await this.platformDeps?.store.get();
|
|
1363
|
-
|
|
1364
|
-
|
|
1456
|
+
// The auth release gates the shared-host organisation check and the cookie domain of every company.
|
|
1457
|
+
const withRefs = (readiness) => platform
|
|
1365
1458
|
? { ...readiness, serviceGitRefs: serviceGitRefsOf(platform, org.versionPin) }
|
|
1366
1459
|
: readiness;
|
|
1460
|
+
if (!ownsServices(org))
|
|
1461
|
+
return withRefs(base);
|
|
1462
|
+
const held = this.platformDeps?.heldKeys;
|
|
1463
|
+
const readiness = orgPlatformReadinessFor(org, base, orgRecordedIds, held ? await heldOwnKeyRefs(held, org) : new Set());
|
|
1464
|
+
return withRefs(readiness);
|
|
1367
1465
|
}
|
|
1368
1466
|
/**
|
|
1369
1467
|
* The org's `heldSecret` envs: the refs that hold a value now, which of those differ from the live
|
|
@@ -1763,6 +1861,73 @@ export class FoundryPlanApply {
|
|
|
1763
1861
|
export function planNeedsConfirmation(steps) {
|
|
1764
1862
|
return steps.some((step) => step.destroys || step.migrates);
|
|
1765
1863
|
}
|
|
1864
|
+
/**
|
|
1865
|
+
* The key of the hosting entry a step belongs to, or `null` for a step of the org or the platform.
|
|
1866
|
+
* An entry's own steps are keyed `<orgId>:<entry key>` (its Coolify app, Worker, Worker app, and
|
|
1867
|
+
* ai, edge and files keys); with `orgId` the prefix must be exactly that id. With `entries` (the
|
|
1868
|
+
* declared ones) two more are known: the ZITADEL sign-in client named like an entry that declares
|
|
1869
|
+
* `uses.auth`, and a gate named in an entry's `gates`. The org database, the org-wide files key,
|
|
1870
|
+
* the customers organisation and the issuer belong to no entry. (`apply` lets an entry's
|
|
1871
|
+
* confirmation pass the park of the org database once no declared entry uses a database.)
|
|
1872
|
+
*/
|
|
1873
|
+
export function stepEntryKey(step, orgId, entries = []) {
|
|
1874
|
+
const { system, kind, key } = step.action;
|
|
1875
|
+
if (system === 'zitadel' && kind === 'app') {
|
|
1876
|
+
return entries.find((e) => e.key === key && e.uses?.auth)?.key ?? null;
|
|
1877
|
+
}
|
|
1878
|
+
const entryOwned = (system === 'coolify' && kind === 'app') ||
|
|
1879
|
+
(system === 'cloudflare' && (kind === 'worker' || kind === 'worker-app' || kind === 'gate')) ||
|
|
1880
|
+
(kind === 'key' && (system === 'ai' || system === 'edge' || system === 'files'));
|
|
1881
|
+
if (!entryOwned)
|
|
1882
|
+
return null;
|
|
1883
|
+
const colon = key.indexOf(':');
|
|
1884
|
+
if (colon < 1)
|
|
1885
|
+
return null;
|
|
1886
|
+
const prefix = key.slice(0, colon);
|
|
1887
|
+
if (orgId === undefined ? prefix === 'platform' : prefix !== orgId)
|
|
1888
|
+
return null;
|
|
1889
|
+
const rest = key.slice(colon + 1);
|
|
1890
|
+
if (rest === '' || rest.includes(':'))
|
|
1891
|
+
return null;
|
|
1892
|
+
if (kind === 'gate')
|
|
1893
|
+
return entries.find((e) => e.gates?.some((g) => g.key === rest))?.key ?? null;
|
|
1894
|
+
return rest;
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* The step that parks the org's one database. It belongs to the org, not to an entry, but the
|
|
1898
|
+
* entry that removes the last use of it may confirm it (see `entryConfirms`).
|
|
1899
|
+
*/
|
|
1900
|
+
function isOrgDatabasePark(step, orgId) {
|
|
1901
|
+
const { system, kind, key } = step.action;
|
|
1902
|
+
return system === 'coolify' && kind === 'database' && key === orgId && step.step.destroys;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* The delete of one of the org's gates that no entry declares any more. The step names only the
|
|
1906
|
+
* gate, so the caller names the removed entry's gates (`entryConfirmation.gates`) and only those pass.
|
|
1907
|
+
*/
|
|
1908
|
+
function isUndeclaredGateDelete(step, orgId) {
|
|
1909
|
+
const { system, kind, key, desired } = step.action;
|
|
1910
|
+
return (system === 'cloudflare' &&
|
|
1911
|
+
kind === 'gate' &&
|
|
1912
|
+
key.startsWith(`${orgId}:`) &&
|
|
1913
|
+
desired?.mode === 'delete');
|
|
1914
|
+
}
|
|
1915
|
+
/** Whether `confirmation` passes every destroying or migrating step of `steps`: all belong to its entry, and it typed the key. */
|
|
1916
|
+
export function entryConfirms(confirmation, steps, orgId, entries) {
|
|
1917
|
+
if (!confirmation ||
|
|
1918
|
+
confirmation.entryKey === '' ||
|
|
1919
|
+
confirmation.typed !== confirmation.entryKey) {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
return steps
|
|
1923
|
+
.filter(({ step }) => step.destroys || step.migrates)
|
|
1924
|
+
.every((planned) => stepEntryKey(planned, orgId, entries) === confirmation.entryKey ||
|
|
1925
|
+
(isOrgDatabasePark(planned, orgId) && !entries.some(entryHasDatabase)) ||
|
|
1926
|
+
(isUndeclaredGateDelete(planned, orgId) &&
|
|
1927
|
+
!planned.step.migrates &&
|
|
1928
|
+
(confirmation.gates ?? []).includes(planned.action.key.slice(orgId.length + 1)) &&
|
|
1929
|
+
!entries.some((e) => e.key === confirmation.entryKey)));
|
|
1930
|
+
}
|
|
1766
1931
|
/** `AppliedState.serviceIds`' two flat ZITADEL scope keys (`executor.ts`'s `ensureZitadelProject`/`zitadelIssuer`) -- no `:`, unlike a resource key. */
|
|
1767
1932
|
const ZITADEL_PROJECT_SCOPE_KEY = 'zitadel_project';
|
|
1768
1933
|
const ZITADEL_ORG_SCOPE_KEY = 'zitadel_org';
|
|
@@ -2022,6 +2187,7 @@ const STEP_FIELDS = {
|
|
|
2022
2187
|
'zitadel:owner': 'owners',
|
|
2023
2188
|
'zitadel:customers': 'hosting.entries.uses.auth',
|
|
2024
2189
|
'zitadel:id-token-userinfo': 'hosting.entries.uses.auth',
|
|
2190
|
+
'zitadel:desktop-callback': 'hosting.entries.uses.auth',
|
|
2025
2191
|
'zitadel:mcp-project': 'hosting.entries.uses.auth.mcp',
|
|
2026
2192
|
'zitadel:mcp-access': 'hosting.entries.uses.auth.mcp',
|
|
2027
2193
|
'zitadel:access-token-lifetime': 'hosting.entries.uses.auth.mcp',
|
|
@@ -60,6 +60,12 @@ export declare const PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY = "auth_z
|
|
|
60
60
|
export declare const AUTH_ZITADEL_RUN_OPTIONS = "--entrypoint \"/app/zitadel start-from-init --masterkeyFromEnv --tlsMode external\"";
|
|
61
61
|
/** The run options last applied to the ZITADEL app (value: the options string), so a changed value forces one redeploy. */
|
|
62
62
|
export declare const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = "zitadel_platform_auth_run_options_applied";
|
|
63
|
+
/**
|
|
64
|
+
* The `gitRef` the platform's login, in-app login and broker apps were last built from (value: the ref). A
|
|
65
|
+
* different declared ref plans the auth step again: it patches `git_branch` on those three apps and
|
|
66
|
+
* redeploys them, so a release tag named in the declaration reaches the running login.
|
|
67
|
+
*/
|
|
68
|
+
export declare const PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY = "platform_auth_applied_git_ref";
|
|
63
69
|
/**
|
|
64
70
|
* #251: the Synapse/MAS image tags an org's calls Matrix stack last
|
|
65
71
|
* successful apply actually set live. `coolifyCalls` (`executor.ts`)
|
|
@@ -611,6 +617,31 @@ export declare const PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY = "zitadel_platform_a
|
|
|
611
617
|
* repair while this differs from the recorded client app id.
|
|
612
618
|
*/
|
|
613
619
|
export declare const PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY = "archon_id_token_userinfo_applied";
|
|
620
|
+
/**
|
|
621
|
+
* The marker for Archon's sign-in client once its post-logout addresses were
|
|
622
|
+
* set: the client's ZITADEL app id and the set it holds
|
|
623
|
+
* (`postLogoutAppliedValue`). `planArchonPostLogoutUris` plans the update
|
|
624
|
+
* while this differs from what the declared hostname wants.
|
|
625
|
+
*/
|
|
626
|
+
export declare const PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY = "archon_post_logout_applied";
|
|
627
|
+
/**
|
|
628
|
+
* #394 (decision D2, release one): the role key on the platform's `mail`
|
|
629
|
+
* ZITADEL project that every mailbox user holds once the project's role check
|
|
630
|
+
* is on. This release only makes the role and grants it; the check stays off.
|
|
631
|
+
*/
|
|
632
|
+
export declare const PLATFORM_MAIL_USER_ROLE = "mail:use";
|
|
633
|
+
/** The mail project id once {@link PLATFORM_MAIL_USER_ROLE} exists on it. */
|
|
634
|
+
export declare const PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY = "zitadel_platform_mail_role";
|
|
635
|
+
/** `<mail project id>|<owner user id, or none>` once the owner holds the role. */
|
|
636
|
+
export declare const PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY = "zitadel_platform_mail_role_granted";
|
|
637
|
+
/** The mail project id once its role check and has-project check are on (release two, behind an option). */
|
|
638
|
+
export declare const PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY = "zitadel_platform_mail_role_check";
|
|
639
|
+
/**
|
|
640
|
+
* The ZITADEL app id of Archon's sign-in client once `/auth/desktop/callback`
|
|
641
|
+
* was registered on it (#339). `planArchonDesktopCallback` plans the step
|
|
642
|
+
* while this differs from the recorded client app id.
|
|
643
|
+
*/
|
|
644
|
+
export declare const PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY = "archon_desktop_callback_applied";
|
|
614
645
|
/**
|
|
615
646
|
* The settings `coolifyApp` owns on an entry's app (`folder`, `port`,
|
|
616
647
|
* `autoDeploy`) as the JSON the marker below holds. `coolifyApp` records it
|
|
@@ -1225,6 +1256,18 @@ export interface PlanPlatformOptions {
|
|
|
1225
1256
|
* repair step is not planned.
|
|
1226
1257
|
*/
|
|
1227
1258
|
archonIdTokenUserinfo?: boolean;
|
|
1259
|
+
/** The owner company's ZITADEL client has `updateOidcAppPostLogoutRedirectUris`. */
|
|
1260
|
+
archonPostLogoutUris?: boolean;
|
|
1261
|
+
/**
|
|
1262
|
+
* The Archon sign-in client's ZITADEL client has `getOidcAppRedirectUris`
|
|
1263
|
+
* and `updateOidcAppRedirectUris`. Not a block: without them the desktop
|
|
1264
|
+
* callback step is not planned.
|
|
1265
|
+
*/
|
|
1266
|
+
archonDesktopCallback?: boolean;
|
|
1267
|
+
/** The mail step's ZITADEL client can make a project role and grant it. Not a block: without it no mail role step is planned. */
|
|
1268
|
+
mailRole?: boolean;
|
|
1269
|
+
/** The host asked for the mail project's role check (release two) and its client has `setProjectRoleChecks`. */
|
|
1270
|
+
mailRoleCheck?: boolean;
|
|
1228
1271
|
/**
|
|
1229
1272
|
* The executor has the platform's sign-in dependencies and a ZITADEL
|
|
1230
1273
|
* client for its instance. Not a block: without it the sign-in policy
|
|
@@ -1265,6 +1308,8 @@ export interface PlanPlatformOptions {
|
|
|
1265
1308
|
drifted?: {
|
|
1266
1309
|
archon?: boolean;
|
|
1267
1310
|
auth?: boolean;
|
|
1311
|
+
/** #643: a recorded sign-in app is missing or not running; plans the step again, minting nothing. */
|
|
1312
|
+
authApps?: boolean;
|
|
1268
1313
|
edge?: boolean;
|
|
1269
1314
|
files?: boolean;
|
|
1270
1315
|
backupStorage?: boolean;
|
|
@@ -12,6 +12,7 @@ import { PLATFORM_MAIL_RELAY_KEY, planMailRelay } from './mail-relay.js';
|
|
|
12
12
|
import { MAIL_DATABASE_SCOPE_KEY, mailStoreIsLegacy, mailStoreSwitchChange, mailStoreSwitchDescription, } from './mail-store.js';
|
|
13
13
|
import { planOikosIngress } from './oikos-ingress.js';
|
|
14
14
|
import { callsSfuAppliedFingerprint } from './platform-sfu.js';
|
|
15
|
+
import { postLogoutUpdateOwed, postLogoutUrisFor } from './post-logout-uris.js';
|
|
15
16
|
import { archonInAppHost } from './sign-in-archon.js';
|
|
16
17
|
import { signInDomainBlockedText } from './sign-in-org.js';
|
|
17
18
|
import { withStepSystem } from './step-system.js';
|
|
@@ -64,6 +65,12 @@ export const PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY = 'auth_zitadel_a
|
|
|
64
65
|
export const AUTH_ZITADEL_RUN_OPTIONS = '--entrypoint "/app/zitadel start-from-init --masterkeyFromEnv --tlsMode external"';
|
|
65
66
|
/** The run options last applied to the ZITADEL app (value: the options string), so a changed value forces one redeploy. */
|
|
66
67
|
export const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = 'zitadel_platform_auth_run_options_applied';
|
|
68
|
+
/**
|
|
69
|
+
* The `gitRef` the platform's login, in-app login and broker apps were last built from (value: the ref). A
|
|
70
|
+
* different declared ref plans the auth step again: it patches `git_branch` on those three apps and
|
|
71
|
+
* redeploys them, so a release tag named in the declaration reaches the running login.
|
|
72
|
+
*/
|
|
73
|
+
export const PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY = 'platform_auth_applied_git_ref';
|
|
67
74
|
/**
|
|
68
75
|
* #251: the Synapse/MAS image tags an org's calls Matrix stack last
|
|
69
76
|
* successful apply actually set live. `coolifyCalls` (`executor.ts`)
|
|
@@ -694,6 +701,31 @@ export const PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY = 'zitadel_platform_access_ap
|
|
|
694
701
|
* repair while this differs from the recorded client app id.
|
|
695
702
|
*/
|
|
696
703
|
export const PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY = 'archon_id_token_userinfo_applied';
|
|
704
|
+
/**
|
|
705
|
+
* The marker for Archon's sign-in client once its post-logout addresses were
|
|
706
|
+
* set: the client's ZITADEL app id and the set it holds
|
|
707
|
+
* (`postLogoutAppliedValue`). `planArchonPostLogoutUris` plans the update
|
|
708
|
+
* while this differs from what the declared hostname wants.
|
|
709
|
+
*/
|
|
710
|
+
export const PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY = 'archon_post_logout_applied';
|
|
711
|
+
/**
|
|
712
|
+
* #394 (decision D2, release one): the role key on the platform's `mail`
|
|
713
|
+
* ZITADEL project that every mailbox user holds once the project's role check
|
|
714
|
+
* is on. This release only makes the role and grants it; the check stays off.
|
|
715
|
+
*/
|
|
716
|
+
export const PLATFORM_MAIL_USER_ROLE = 'mail:use';
|
|
717
|
+
/** The mail project id once {@link PLATFORM_MAIL_USER_ROLE} exists on it. */
|
|
718
|
+
export const PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY = 'zitadel_platform_mail_role';
|
|
719
|
+
/** `<mail project id>|<owner user id, or none>` once the owner holds the role. */
|
|
720
|
+
export const PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY = 'zitadel_platform_mail_role_granted';
|
|
721
|
+
/** The mail project id once its role check and has-project check are on (release two, behind an option). */
|
|
722
|
+
export const PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY = 'zitadel_platform_mail_role_check';
|
|
723
|
+
/**
|
|
724
|
+
* The ZITADEL app id of Archon's sign-in client once `/auth/desktop/callback`
|
|
725
|
+
* was registered on it (#339). `planArchonDesktopCallback` plans the step
|
|
726
|
+
* while this differs from the recorded client app id.
|
|
727
|
+
*/
|
|
728
|
+
export const PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY = 'archon_desktop_callback_applied';
|
|
697
729
|
export function appSettingsOf(entry) {
|
|
698
730
|
return {
|
|
699
731
|
folder: entry.folder,
|
|
@@ -1324,7 +1356,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1324
1356
|
// reconcile below can be planned in the *same* round hc first comes up,
|
|
1325
1357
|
// the same way files/mail/archon are planned in auth's own first round.
|
|
1326
1358
|
const hcReady = hc.step !== null || Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_HC_KEY)]);
|
|
1327
|
-
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite);
|
|
1359
|
+
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false);
|
|
1328
1360
|
if (auth.step)
|
|
1329
1361
|
plannedSteps.push(auth.step);
|
|
1330
1362
|
// #174: the integrations host. Planned after auth; neither step reads the
|
|
@@ -1414,6 +1446,20 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1414
1446
|
const idTokenUserinfo = planArchonIdTokenUserinfo(platform, recordedIds, blocked.archonIdTokenUserinfo ?? false);
|
|
1415
1447
|
if (idTokenUserinfo)
|
|
1416
1448
|
plannedSteps.push(idTokenUserinfo);
|
|
1449
|
+
const postLogoutUris = planArchonPostLogoutUris(platform, recordedIds, blocked.archonPostLogoutUris ?? false);
|
|
1450
|
+
if (postLogoutUris)
|
|
1451
|
+
plannedSteps.push(postLogoutUris);
|
|
1452
|
+
// #339: Archon's sign-in client takes the desktop callback too.
|
|
1453
|
+
const desktopCallback = planArchonDesktopCallback(platform, recordedIds, blocked.archonDesktopCallback ?? false);
|
|
1454
|
+
if (desktopCallback)
|
|
1455
|
+
plannedSteps.push(desktopCallback);
|
|
1456
|
+
// #394: the mail role and its grants. After the auth and mail steps, which record what it reads.
|
|
1457
|
+
const mailRole = planMailRole(platform, recordedIds, {
|
|
1458
|
+
role: blocked.mailRole ?? false,
|
|
1459
|
+
check: blocked.mailRoleCheck ?? false,
|
|
1460
|
+
});
|
|
1461
|
+
if (mailRole.step)
|
|
1462
|
+
plannedSteps.push(mailRole.step);
|
|
1417
1463
|
// #669: after the ID token repair too, which is proven; a policy call that fails every time holds up nothing.
|
|
1418
1464
|
const signInPolicy = planAuthSignInPolicy(platform, recordedIds, blocked.authSignInPolicy ?? false);
|
|
1419
1465
|
if (signInPolicy)
|
|
@@ -1481,6 +1527,8 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1481
1527
|
unmanaged.push({ description: auth.blocked, key: 'auth' });
|
|
1482
1528
|
if (mail.blocked)
|
|
1483
1529
|
unmanaged.push({ description: mail.blocked, key: 'mail' });
|
|
1530
|
+
if (mailRole.blocked)
|
|
1531
|
+
unmanaged.push({ description: mailRole.blocked, key: 'mail-role' });
|
|
1484
1532
|
if (relay.blocked)
|
|
1485
1533
|
unmanaged.push({ description: relay.blocked, key: 'mail-relay' });
|
|
1486
1534
|
if (platform.mail.relay === true) {
|
|
@@ -1946,7 +1994,7 @@ function planSfu(platform, callsOrgSlugs, recordedIds) {
|
|
|
1946
1994
|
* promising "this apply re-mints" while one of those is still missing would
|
|
1947
1995
|
* be wrong the moment it is read.
|
|
1948
1996
|
*/
|
|
1949
|
-
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked) {
|
|
1997
|
+
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false) {
|
|
1950
1998
|
if (!platform.auth.enabled)
|
|
1951
1999
|
return { step: null, blocked: null };
|
|
1952
2000
|
// C3/S2: standing notes. The invite and the domain wait on a person; they hold nothing else back.
|
|
@@ -1977,7 +2025,14 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
1977
2025
|
// comment for why this never holds the step back the way the admin key's
|
|
1978
2026
|
// own `undelivered` check below does.
|
|
1979
2027
|
const loginClientUndelivered = recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY];
|
|
1980
|
-
if (
|
|
2028
|
+
if (
|
|
2029
|
+
// #643: the apps recorded, but one is gone or stopped. The step finds each by name, deploys what is not
|
|
2030
|
+
// running and mints nothing while the records stand.
|
|
2031
|
+
!appsDown &&
|
|
2032
|
+
// #741: a refused owner-org domain (blocked key recorded, domain not) is tried again on the next apply.
|
|
2033
|
+
// While it stays refused the step is planned on every run, each spending about five seconds; known, no code.
|
|
2034
|
+
ownerDomainBlockedText(recordedIds) === null &&
|
|
2035
|
+
recordedIds[key] &&
|
|
1981
2036
|
connectorApplied &&
|
|
1982
2037
|
brokerApplied &&
|
|
1983
2038
|
brokerDeployed &&
|
|
@@ -1995,7 +2050,8 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
1995
2050
|
recordedIds[PLATFORM_OWNER_ORG_SCOPE_KEY]) &&
|
|
1996
2051
|
// The ZITADEL run options reach the container only through the auth
|
|
1997
2052
|
// step's redeploy, so a platform recorded before them still plans it.
|
|
1998
|
-
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS
|
|
2053
|
+
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
2054
|
+
recordedIds[PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY] === platform.auth.gitRef) {
|
|
1999
2055
|
return { step: null, blocked: standingNote };
|
|
2000
2056
|
}
|
|
2001
2057
|
// The admin key was minted but never reached the host. ZITADEL shows a
|
|
@@ -2542,6 +2598,97 @@ function planArchonIdTokenUserinfo(platform, recordedIds, wired) {
|
|
|
2542
2598
|
},
|
|
2543
2599
|
};
|
|
2544
2600
|
}
|
|
2601
|
+
/**
|
|
2602
|
+
* #394, decision D2 (release one): one `zitadel:mail-role` step keyed
|
|
2603
|
+
* {@link PLATFORM_MAIL_KEY}. It makes the role {@link PLATFORM_MAIL_USER_ROLE}
|
|
2604
|
+
* on the mail project, grants it to the platform owner, and (only when the host
|
|
2605
|
+
* asks, `wired.check`) turns on the project's role check on a later run than the
|
|
2606
|
+
* grant. It waits until the mail project, the platform's ZITADEL org and the
|
|
2607
|
+
* owner invite are recorded.
|
|
2608
|
+
*
|
|
2609
|
+
* The owner's user lives in the owner company's sign-in org, and the mail project
|
|
2610
|
+
* in the platform's own org. A role grant on a project of another org needs a
|
|
2611
|
+
* project grant with role keys, which the ports do not have; the owner's grant
|
|
2612
|
+
* is reported blocked, not attempted, and the role is still made.
|
|
2613
|
+
*/
|
|
2614
|
+
function planMailRole(platform, recordedIds, wired) {
|
|
2615
|
+
if (!wired.role || !platform.mail.enabled || !platform.auth.enabled) {
|
|
2616
|
+
return { step: null, blocked: null };
|
|
2617
|
+
}
|
|
2618
|
+
const projectId = recordedIds[PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY];
|
|
2619
|
+
const orgId = recordedIds[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
|
|
2620
|
+
const invite = recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
|
|
2621
|
+
if (!projectId || !orgId || !invite)
|
|
2622
|
+
return { step: null, blocked: null };
|
|
2623
|
+
const sameOrg = invite === 'none' || recordedIds[PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY] === orgId;
|
|
2624
|
+
const blocked = sameOrg
|
|
2625
|
+
? null
|
|
2626
|
+
: "the platform owner's mail role is blocked, not planned: the owner is in another ZITADEL org than the mail project, and a role grant across orgs needs a project grant with role keys, which is not built";
|
|
2627
|
+
const needsRole = recordedIds[PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY] !== projectId;
|
|
2628
|
+
const granted = recordedIds[PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY] === `${projectId}|${invite}`;
|
|
2629
|
+
// The check is planned only after a grant an earlier apply made, never with the first grant,
|
|
2630
|
+
// and never while the owner holds no role (it would lock the owner out).
|
|
2631
|
+
const needsCheck = wired.check &&
|
|
2632
|
+
sameOrg &&
|
|
2633
|
+
granted &&
|
|
2634
|
+
recordedIds[PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY] !== projectId;
|
|
2635
|
+
if (!needsRole && !(sameOrg && !granted) && !needsCheck)
|
|
2636
|
+
return { step: null, blocked };
|
|
2637
|
+
return {
|
|
2638
|
+
step: {
|
|
2639
|
+
action: { system: 'zitadel', kind: 'mail-role', key: PLATFORM_MAIL_KEY, desired: {} },
|
|
2640
|
+
step: {
|
|
2641
|
+
description: `make the mail role '${PLATFORM_MAIL_USER_ROLE}' on the mail project and grant it to the platform owner`,
|
|
2642
|
+
destroys: false,
|
|
2643
|
+
migrates: false,
|
|
2644
|
+
},
|
|
2645
|
+
},
|
|
2646
|
+
blocked,
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
function planArchonDesktopCallback(platform, recordedIds, wired) {
|
|
2650
|
+
if (!wired || !platform.archon.enabled)
|
|
2651
|
+
return null;
|
|
2652
|
+
const appId = recordedIds[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
|
|
2653
|
+
if (!appId || recordedIds[PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY] === appId) {
|
|
2654
|
+
return null;
|
|
2655
|
+
}
|
|
2656
|
+
return {
|
|
2657
|
+
action: { system: 'zitadel', kind: 'desktop-callback', key: PLATFORM_ARCHON_KEY, desired: {} },
|
|
2658
|
+
step: {
|
|
2659
|
+
description: "register the desktop sign-in callback on Archon's sign-in client",
|
|
2660
|
+
destroys: false,
|
|
2661
|
+
migrates: false,
|
|
2662
|
+
},
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Archon's sign-in client holds its root and `/auth/signed-out` as post-logout
|
|
2667
|
+
* addresses. One `zitadel:post-logout-uris` step keyed {@link PLATFORM_ARCHON_KEY},
|
|
2668
|
+
* planned while {@link PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY} differs
|
|
2669
|
+
* from the recorded client app id and the declared hostname's set (a client
|
|
2670
|
+
* made again records it at creation). Not planned when the host's ZITADEL
|
|
2671
|
+
* client lacks the method, and not blocked either.
|
|
2672
|
+
*/
|
|
2673
|
+
function planArchonPostLogoutUris(platform, recordedIds, wired) {
|
|
2674
|
+
if (!wired || !platform.archon.enabled)
|
|
2675
|
+
return null;
|
|
2676
|
+
const appId = recordedIds[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
|
|
2677
|
+
if (!appId)
|
|
2678
|
+
return null;
|
|
2679
|
+
const uris = postLogoutUrisFor([platform.archon.hostname]);
|
|
2680
|
+
if (!postLogoutUpdateOwed(recordedIds[PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY], appId, uris)) {
|
|
2681
|
+
return null;
|
|
2682
|
+
}
|
|
2683
|
+
return {
|
|
2684
|
+
action: { system: 'zitadel', kind: 'post-logout-uris', key: PLATFORM_ARCHON_KEY, desired: {} },
|
|
2685
|
+
step: {
|
|
2686
|
+
description: "update the post-logout addresses of Archon's sign-in client",
|
|
2687
|
+
destroys: false,
|
|
2688
|
+
migrates: false,
|
|
2689
|
+
},
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2545
2692
|
/**
|
|
2546
2693
|
* #394, #669: the platform's sign-in org reserves the username `admin` and
|
|
2547
2694
|
* switches self-registration off. One `zitadel:sign-in-policy` step keyed
|
|
@@ -3061,6 +3208,14 @@ function planArchon(platform, recordedIds, executorBlocked, ownerSignIn, ownerFi
|
|
|
3061
3208
|
// A deploy the step triggered is still running: planning it again would deploy twice.
|
|
3062
3209
|
if (deployRunning)
|
|
3063
3210
|
return { step: null, blocked: deployRunning };
|
|
3211
|
+
// Giving Archon's address up edits the in-app login app's host table: without that app's id the binding
|
|
3212
|
+
// cannot be removed, so nothing is planned (and the address is not recorded as given up).
|
|
3213
|
+
if (ownerSignIn && !ownerSignIn.signInLoginAppId && archonInAppHost(recordedIds) !== '') {
|
|
3214
|
+
return {
|
|
3215
|
+
step: null,
|
|
3216
|
+
blocked: "Archon's own sign-in address cannot be given up: the owner company's in-app login app is not recorded",
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3064
3219
|
const key = resourceKey('coolify', 'app', PLATFORM_ARCHON_KEY);
|
|
3065
3220
|
const archonOrgId = recordedIds[PLATFORM_ARCHON_ORG_SCOPE_KEY];
|
|
3066
3221
|
const applied = Boolean(recordedIds[key]) &&
|
|
@@ -104,6 +104,8 @@ export interface OrgPlatformReadiness {
|
|
|
104
104
|
platformZitadelIds?: readonly string[];
|
|
105
105
|
/** The second login app that serves `/sign-in` for companies with `signIn.inApp` is applied. Absent reads as false. */
|
|
106
106
|
signInLoginReady?: boolean;
|
|
107
|
+
/** `LOGIN_SESSION_BIND_KEY` is live on the in-app login app (`sign_in_bind_key_ready`). Absent reads as false. */
|
|
108
|
+
signInBindKeyReady?: boolean;
|
|
107
109
|
/** The platform's own sign-in (#168), which every org's own ZITADEL org lives inside. */
|
|
108
110
|
authReady: boolean;
|
|
109
111
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ownsServices } from '../org.js';
|
|
2
2
|
import { ZITADEL_ORG_SCOPE_KEY, agoraHostnameFor } from './agora.js';
|
|
3
|
+
import { SIGN_IN_BIND_KEY_SCOPE_KEY } from './cookie-domain.js';
|
|
3
4
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
4
5
|
import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
5
6
|
import { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, ownAuthApplied } from './login-host.js';
|
|
@@ -161,6 +162,9 @@ edgePortPresent = false) {
|
|
|
161
162
|
? { platformAuthRecordedHost: platform.auth.hostname }
|
|
162
163
|
: {}),
|
|
163
164
|
...(platformRecordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY] ? { signInLoginReady: true } : {}),
|
|
165
|
+
...(platformRecordedIds[SIGN_IN_BIND_KEY_SCOPE_KEY] === '1'
|
|
166
|
+
? { signInBindKeyReady: true }
|
|
167
|
+
: {}),
|
|
164
168
|
authReady,
|
|
165
169
|
authIssuer: authReady ? `https://${platform.auth.hostname}` : null,
|
|
166
170
|
jobsReady: jobsApplied && jobsUrl !== undefined && missingKeys.jobs === null,
|
|
@@ -391,6 +391,10 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
391
391
|
archonRunsInsideApp?: boolean;
|
|
392
392
|
archonDeployRunning?: string;
|
|
393
393
|
archonIdTokenUserinfo?: boolean;
|
|
394
|
+
archonPostLogoutUris?: boolean;
|
|
395
|
+
archonDesktopCallback?: boolean;
|
|
396
|
+
mailRole?: boolean;
|
|
397
|
+
mailRoleCheck?: boolean;
|
|
394
398
|
authSignInPolicy?: boolean;
|
|
395
399
|
archonOrg?: string;
|
|
396
400
|
archonOrgInvite?: string;
|
|
@@ -716,7 +720,13 @@ export interface PlatformMailDeps {
|
|
|
716
720
|
* checks an access token's `aud` claim against -- #346, no host has to
|
|
717
721
|
* register a resource API for this by hand any more.
|
|
718
722
|
*/
|
|
719
|
-
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject'
|
|
723
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject'> & Partial<Pick<ZitadelPort, 'ensureProjectRole' | 'grantProjectRoles' | 'setProjectRoleChecks'>>;
|
|
724
|
+
/**
|
|
725
|
+
* #394 (decision D2): turn on the mail project's role check and has-project check. Default off:
|
|
726
|
+
* release one only makes the role and grants it, and the check follows in release two, on a
|
|
727
|
+
* run after the grant. Needs `setProjectRoleChecks` on the client.
|
|
728
|
+
*/
|
|
729
|
+
requireMailRole?: boolean;
|
|
720
730
|
/** {@link PlatformArchonDeps.zitadelForInstance}, for Stalwart's OIDC directory. */
|
|
721
731
|
zitadelForInstance?: (issuerUrl: string) => PlatformMailDeps['zitadel'];
|
|
722
732
|
/**
|
|
@@ -917,7 +927,7 @@ export interface PlatformArchonDeps {
|
|
|
917
927
|
access: PlatformAccessFactory;
|
|
918
928
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
919
929
|
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
920
|
-
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'> & Partial<Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'allowPerAppLoginBaseUri'>>;
|
|
930
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'> & Partial<Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'allowPerAppLoginBaseUri' | 'updateOidcAppPostLogoutRedirectUris' | 'getOidcAppRedirectUris' | 'updateOidcAppRedirectUris'>>;
|
|
921
931
|
/**
|
|
922
932
|
* {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client:
|
|
923
933
|
* the owner company's own ZITADEL, by its issuer and the held-key reference of
|