@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.
Files changed (78) hide show
  1. package/dist/build-json.d.ts +28 -0
  2. package/dist/build-json.js +95 -0
  3. package/dist/index.d.ts +10 -4
  4. package/dist/index.js +6 -2
  5. package/dist/migrations/0058_app_requests.sql +32 -0
  6. package/dist/org.d.ts +68 -5
  7. package/dist/org.js +287 -135
  8. package/dist/plan/agora.d.ts +14 -0
  9. package/dist/plan/agora.js +21 -0
  10. package/dist/plan/backup-gate.d.ts +15 -0
  11. package/dist/plan/backup-gate.js +38 -0
  12. package/dist/plan/boule-owners-http.d.ts +12 -0
  13. package/dist/plan/boule-owners-http.js +59 -0
  14. package/dist/plan/boule.d.ts +38 -2
  15. package/dist/plan/boule.js +72 -1
  16. package/dist/plan/company-backups.d.ts +7 -1
  17. package/dist/plan/company-backups.js +42 -2
  18. package/dist/plan/cookie-domain.d.ts +41 -0
  19. package/dist/plan/cookie-domain.js +71 -0
  20. package/dist/plan/desktop-auth.d.ts +58 -0
  21. package/dist/plan/desktop-auth.js +144 -0
  22. package/dist/plan/desktop-callback.d.ts +26 -0
  23. package/dist/plan/desktop-callback.js +59 -0
  24. package/dist/plan/diff.d.ts +13 -4
  25. package/dist/plan/diff.js +277 -88
  26. package/dist/plan/executor.d.ts +136 -6
  27. package/dist/plan/executor.js +732 -105
  28. package/dist/plan/health-gaps.js +5 -0
  29. package/dist/plan/health-targets.js +1 -2
  30. package/dist/plan/hostnames.d.ts +2 -2
  31. package/dist/plan/hostnames.js +6 -6
  32. package/dist/plan/index.d.ts +4 -1
  33. package/dist/plan/index.js +3 -1
  34. package/dist/plan/job-app.d.ts +23 -0
  35. package/dist/plan/job-app.js +41 -0
  36. package/dist/plan/live.d.ts +6 -0
  37. package/dist/plan/live.js +16 -0
  38. package/dist/plan/login-host.d.ts +28 -0
  39. package/dist/plan/login-host.js +36 -0
  40. package/dist/plan/org-names.js +9 -1
  41. package/dist/plan/org-postgres-job.d.ts +22 -0
  42. package/dist/plan/org-postgres-job.js +91 -0
  43. package/dist/plan/org-postgres-migrate.d.ts +115 -0
  44. package/dist/plan/org-postgres-migrate.js +466 -0
  45. package/dist/plan/org-postgres.d.ts +90 -0
  46. package/dist/plan/org-postgres.js +293 -0
  47. package/dist/plan/plan-apply.d.ts +60 -3
  48. package/dist/plan/plan-apply.js +174 -8
  49. package/dist/plan/platform-diff.d.ts +45 -0
  50. package/dist/plan/platform-diff.js +159 -4
  51. package/dist/plan/platform-org.d.ts +2 -0
  52. package/dist/plan/platform-org.js +4 -0
  53. package/dist/plan/platform-providers.d.ts +12 -2
  54. package/dist/plan/platform-providers.js +282 -9
  55. package/dist/plan/post-logout-uris.d.ts +22 -0
  56. package/dist/plan/post-logout-uris.js +43 -0
  57. package/dist/plan/providers-readers.d.ts +10 -1
  58. package/dist/plan/providers-readers.js +56 -5
  59. package/dist/plan/sign-in-bindings.d.ts +4 -0
  60. package/dist/plan/sign-in-bindings.js +10 -0
  61. package/dist/plan/sign-in-org.d.ts +19 -6
  62. package/dist/plan/sign-in-org.js +35 -10
  63. package/dist/plan/sign-in.d.ts +12 -5
  64. package/dist/plan/sign-in.js +12 -5
  65. package/dist/plan/signin-db-move.js +10 -34
  66. package/dist/plan/worker-app.js +3 -1
  67. package/dist/refuse-org.d.ts +7 -0
  68. package/dist/refuse-org.js +10 -0
  69. package/dist/requested-changes.d.ts +50 -4
  70. package/dist/requested-changes.js +9 -1
  71. package/dist/store/postgres.d.ts +5 -1
  72. package/dist/store/postgres.js +67 -8
  73. package/dist/store/schema.d.ts +72 -0
  74. package/dist/store/schema.js +4 -0
  75. package/dist/store.d.ts +24 -1
  76. package/dist/worker-app-config.d.ts +8 -2
  77. package/dist/worker-app-config.js +20 -6
  78. package/package.json +5 -5
@@ -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
- const readiness = orgPlatformReadinessFor(org, base, orgRecordedIds, held ? await heldOwnKeyRefs(held, org) : new Set());
1364
- return platform
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 (recordedIds[key] &&
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