@wtfalch/foundry 0.65.0 → 0.66.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 (58) hide show
  1. package/dist/apps/cloudflare.d.ts +5 -0
  2. package/dist/apps/deploy.js +33 -1
  3. package/dist/apps/migrations.d.ts +3 -0
  4. package/dist/apps/migrations.js +32 -7
  5. package/dist/entry-rules.d.ts +11 -0
  6. package/dist/entry-rules.js +70 -0
  7. package/dist/index.d.ts +7 -3
  8. package/dist/index.js +4 -2
  9. package/dist/migrations/0069_release_events.sql +16 -0
  10. package/dist/modules/database.js +22 -3
  11. package/dist/native-build.d.ts +16 -12
  12. package/dist/native-build.js +16 -12
  13. package/dist/plan/boule.js +2 -0
  14. package/dist/plan/diff.d.ts +2 -0
  15. package/dist/plan/diff.js +17 -3
  16. package/dist/plan/executor.d.ts +43 -6
  17. package/dist/plan/executor.js +89 -18
  18. package/dist/plan/login-host.d.ts +8 -0
  19. package/dist/plan/login-host.js +8 -0
  20. package/dist/plan/plan-apply.d.ts +9 -0
  21. package/dist/plan/plan-apply.js +31 -1
  22. package/dist/plan/platform-diff.js +5 -0
  23. package/dist/plan/platform-org.d.ts +6 -0
  24. package/dist/plan/platform-providers.d.ts +8 -6
  25. package/dist/plan/platform-providers.js +21 -8
  26. package/dist/plan/release-host.d.ts +30 -1
  27. package/dist/plan/release-host.js +84 -41
  28. package/dist/plan/release-series-reader.d.ts +26 -0
  29. package/dist/plan/release-series-reader.js +40 -0
  30. package/dist/plan/sign-in-archon.d.ts +1 -1
  31. package/dist/plan/sign-in-archon.js +2 -2
  32. package/dist/plan/worker-app-database.d.ts +12 -1
  33. package/dist/plan/worker-app-database.js +13 -0
  34. package/dist/plan/worker-app.d.ts +27 -2
  35. package/dist/plan/worker-app.js +52 -7
  36. package/dist/plan/workspace.d.ts +2 -2
  37. package/dist/plan/workspace.js +3 -3
  38. package/dist/release/decide.d.ts +2 -0
  39. package/dist/release/decide.js +67 -10
  40. package/dist/release/events.d.ts +13 -0
  41. package/dist/release/events.js +67 -0
  42. package/dist/release/health.d.ts +5 -0
  43. package/dist/release/health.js +3 -3
  44. package/dist/release/run.d.ts +7 -2
  45. package/dist/release/run.js +8 -0
  46. package/dist/release/series.d.ts +58 -0
  47. package/dist/release/series.js +65 -0
  48. package/dist/release/state.d.ts +58 -1
  49. package/dist/release/state.js +45 -0
  50. package/dist/release/types.d.ts +44 -0
  51. package/dist/store/postgres.js +29 -3
  52. package/dist/store/schema.d.ts +112 -0
  53. package/dist/store/schema.js +9 -0
  54. package/dist/worker-app-config.d.ts +22 -1
  55. package/dist/worker-app-config.js +71 -13
  56. package/dist/worker-app-migrations.d.ts +6 -2
  57. package/dist/worker-app-migrations.js +1 -0
  58. package/package.json +1 -1
@@ -844,6 +844,11 @@ export interface ZitadelPort {
844
844
  postLogoutRedirectUris: string[];
845
845
  /** `jwt` for an entry with `uses.auth.mcp` (providers 0.11.0). Omitted: opaque. */
846
846
  accessTokenType?: 'jwt' | 'opaque';
847
+ /**
848
+ * The instance's login host. With `loginV2.required` off an app with no base URI of its own lands on
849
+ * the v1 login, so every app foundry makes carries one from the start; a `required` instance ignores it.
850
+ */
851
+ loginBaseUri?: string;
847
852
  }): Promise<{
848
853
  appId: string;
849
854
  clientId: string;
@@ -1183,12 +1188,24 @@ export interface ZitadelPort {
1183
1188
  updateOidcAppLoginBaseUri(orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
1184
1189
  /**
1185
1190
  * Lets each app's own Login V2 base URI count: foundry starts ZITADEL with `LOGINV2_REQUIRED=true`,
1186
- * which makes the instance ignore it. The host sets the instance feature (`PUT /v2/features/instance`
1191
+ * which makes the instance ignore it (`internal/query/oidc_client.go` in ZITADEL v4.17.3, lines 88-92:
1192
+ * with `required` the app's own base URI is overwritten by the instance's). Call it through
1193
+ * {@link allowPerAppLoginBaseUris}, which first gives every app a base URI. The host sets the instance feature (`PUT /v2/features/instance`
1187
1194
  * with `loginV2.required` false and the instance default as `baseUri`). Called before an app is
1188
1195
  * pointed at its own `/sign-in/` on a company that runs its own ZITADEL; the shared instance's
1189
1196
  * bindings port does the same for the platform's. Optional: absent, nothing is called.
1190
1197
  */
1191
1198
  allowPerAppLoginBaseUri?(instanceBaseUri: string): Promise<void>;
1199
+ /** Every organisation of the instance. Optional: {@link allowPerAppLoginBaseUris} refuses without it. */
1200
+ listOrgs?(): Promise<Array<{
1201
+ id: string;
1202
+ state?: string;
1203
+ }>>;
1204
+ /** Every project of an organisation. Optional: {@link allowPerAppLoginBaseUris} refuses without it. */
1205
+ listProjects?(orgId: string): Promise<Array<{
1206
+ id: string;
1207
+ state?: string;
1208
+ }>>;
1192
1209
  /**
1193
1210
  * Occupies `userName` in `orgId` with a machine user that has no role and
1194
1211
  * no token, so nobody can register under that name. Idempotent: a user
@@ -2377,6 +2394,12 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2377
2394
  * the snapshot taken before the repoint still named it.
2378
2395
  */
2379
2396
  private zitadelLoginHost;
2397
+ /**
2398
+ * `{ loginBaseUri }` for an app made in the org's ZITADEL: the instance's own login host (a company's own
2399
+ * `auth.` host, else the shared one). Nothing when the shared host is not known. See
2400
+ * {@link FoundryStepExecutorPorts}' `createOidcApp` `loginBaseUri`.
2401
+ */
2402
+ private instanceLoginBaseUriFor;
2380
2403
  /**
2381
2404
  * The host table of a company's own in-app login app as a bindings port that collects its edits and
2382
2405
  * writes them once on `flush` (one redeploy, none when nothing changed). Used for a company that runs
@@ -2776,12 +2799,14 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2776
2799
  */
2777
2800
  private mintIfAbsent;
2778
2801
  /**
2779
- * Every company's Boule, F3/#970: `NATIVE_BUILD_KEY` is what Boule presents to Archon's
2780
- * native-build route (estate#262) to ask it to dispatch `build-app.yml`. Minted once, like
2781
- * {@link ensureAuthAppKey}, and never rotated; a present-but-empty value (the bug
2782
- * `mintNativeBuildKey`'s own read-then-generate had, see #957) is treated as not set.
2802
+ * foundry#987: nothing reads `NATIVE_BUILD_KEY` any more -- Boule calls Archon's org build
2803
+ * route (`/api/orgs/{orgId}/native/builds`) with its own org credential instead
2804
+ * (`BOULE_ARCHON_CREDENTIAL_ENV`, archon#706, boule#432). This takes the env row away from
2805
+ * every Boule that still has one from #970/#971; `NATIVE_BUILD_KEY_ENV` and
2806
+ * `NATIVE_BUILD_KEY_BYTES` are deprecated and go in the next release (the two-release removal
2807
+ * rule), once no live Boule holds the row any more.
2783
2808
  */
2784
- private ensureNativeBuildKey;
2809
+ private dropNativeBuildKeyEnv;
2785
2810
  /**
2786
2811
  * `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
2787
2812
  * only the first time (task brief), never regenerated or overwritten, so a
@@ -4103,4 +4128,16 @@ export declare function isNoChangesError(error: unknown): boolean;
4103
4128
  * {@link ZitadelPort.updateOidcAppLoginBaseUri} that counts an app already holding the wanted base URI as
4104
4129
  * done: ZITADEL answers that write with "No changes" and a repeat apply must not fail on it.
4105
4130
  */
4131
+ /**
4132
+ * Turns the instance feature `loginV2.required` off so each app's own base URI counts, and makes sure no
4133
+ * app can then land on ZITADEL's v1 login: with `required` off an app without a base URI of its own
4134
+ * (ZITADEL's own console app, an app foundry did not make, an app made later) falls back to v1, and
4135
+ * ZITADEL v4.17.3 gives a `required` instance's base URI to every app but ignores an app's own
4136
+ * (`internal/query/oidc_client.go:88-92`), so the only way is a base URI on every app. Every live OIDC
4137
+ * app of every organisation and project gets `instanceBaseUri` when it has none; an app that has one
4138
+ * keeps it. The apps are set first, while `required` still makes the instance ignore them, so no sign-in
4139
+ * passes through a gap. Without a way to list every app, or with a listing that may be cut short, it
4140
+ * throws before the feature is touched.
4141
+ */
4142
+ export declare function allowPerAppLoginBaseUris(zitadel: Pick<ZitadelPort, 'allowPerAppLoginBaseUri' | 'listOrgs' | 'listProjects' | 'listApps' | 'updateOidcAppLoginBaseUri'>, instanceBaseUri: string): Promise<void>;
4106
4143
  export declare function setOidcAppLoginBaseUri(zitadel: Pick<ZitadelPort, 'updateOidcAppLoginBaseUri'>, orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
@@ -5,7 +5,7 @@ import { EdgeApiError } from '@wtfalch/edge';
5
5
  import { coolifyImageFor } from '../bootstrap/versions.js';
6
6
  import { redactSecrets } from '../calls-sfu-deploy-support.js';
7
7
  import { resolveCallsSfuUrls } from '../calls-sfu.js';
8
- import { NATIVE_BUILD_KEY_BYTES, NATIVE_BUILD_KEY_ENV } from '../native-build.js';
8
+ import { NATIVE_BUILD_KEY_ENV } from '../native-build.js';
9
9
  import { AI_ENTRY_PERMISSIONS, APPS_DOMAIN, APP_ENTRY_KEY, FILES_ENTRY_PERMISSIONS, FULL_COMMIT_SHA, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, appsOn, companyGithubFor, filesCollectionEnvName, heldEnvRef, isLiveEntry, isMainEntry, kosmosOn, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownsServices, sharedSecretRef, } from '../org.js';
10
10
  import { adoptedKeysUnreadable, parseAdoptSnapshot } from './adopt.js';
11
11
  import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, DESKTOP_AUTH_PORT, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatCallsApplied, chatHasUpstreams, chatIntrospectionAppName, desiredAgoraEnv, desktopAuthInternalName, entryClientIdScopeKey, } from './agora.js';
@@ -52,7 +52,7 @@ import { PROXY_LABEL_TOKEN_REF } from './held-key-refs.js';
52
52
  import { appHostname, cmsStudioHostnameFor, coolifyTunnelDomains, entryHostnames, siteHostnameFor, tunnelDnsHostnames, tunnelIngressHostnames, workspaceHost, } from './hostnames.js';
53
53
  import { KEYS_REGISTRAR_DELIVERY_ATTEMPTS, KEYS_REGISTRAR_KEY_DAYS, KEYS_REGISTRAR_KEY_ENV, KEYS_REGISTRAR_MINTED_AT_SCOPE_KEY, KEYS_REGISTRAR_MINT_STARTED_SCOPE_KEY, KEYS_REGISTRAR_SCOPE_KEY, KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY, KEYS_REGISTRAR_URL_APPLIED_SCOPE_KEY, KEYS_REGISTRAR_URL_ENV, } from './keys-registrar.js';
54
54
  import { KOSMOS_CLIENT_ID_SCOPE_KEY, KOSMOS_DATABASE_SCOPE_KEY, KOSMOS_DEPLOYED_SCOPE_KEY, desiredKosmosEnv, kosmosEnvDrift, kosmosHostnameFor, } from './kosmos.js';
55
- import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
55
+ import { LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION, LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
56
56
  import { machineBypassScopeKey } from './machine-paths.js';
57
57
  import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
58
58
  import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, normalizeMailTarget, } from './mail-relay.js';
@@ -1755,6 +1755,7 @@ export class FoundryStepExecutor {
1755
1755
  : {}),
1756
1756
  zones: this.zonePort(),
1757
1757
  dns: this.dnsPort(),
1758
+ secret: (value) => void this.handledSecrets.add(value),
1758
1759
  ...(this.ports.heldKeys ? { heldKeys: this.ports.heldKeys } : {}),
1759
1760
  };
1760
1761
  }
@@ -2354,6 +2355,7 @@ export class FoundryStepExecutor {
2354
2355
  ? loginHostClientIds(org, recorded, activePairs.map((pair) => pair.clientId).filter((id) => !movedIds.has(id)))
2355
2356
  : clientIds;
2356
2357
  const cookieDomain = inAppCookieDomain(action.desired.cookieDomain);
2358
+ let swept = false;
2357
2359
  const writeCompanyBinding = async (ids) => {
2358
2360
  if (ids.length > 0 || activePairs.length === 0) {
2359
2361
  await bindings.upsertBinding({ host, organizationId: zitadelOrgId, clientIds: ids });
@@ -2428,8 +2430,10 @@ export class FoundryStepExecutor {
2428
2430
  await writeClientOrgs();
2429
2431
  // One write for every upsert above (one redeploy of the login), before any app is repointed.
2430
2432
  await bindings.flush?.();
2431
- if (own && activeHosts.size > 0) {
2432
- await this.ports.zitadel.allowPerAppLoginBaseUri?.(`https://${host}/`);
2433
+ // In-app sign-in recorded or wanted: `required` is off, so every app needs a base URI (marker below).
2434
+ if (own && (activeHosts.size > 0 || recorded[SIGN_IN_IN_APP_SCOPE_KEY])) {
2435
+ await allowPerAppLoginBaseUris(this.ports.zitadel, `https://${host}/`);
2436
+ swept = Boolean(this.ports.zitadel.allowPerAppLoginBaseUri);
2433
2437
  }
2434
2438
  }
2435
2439
  // The base URI each app's sign-in starts from: its own address under `/sign-in/` in-app, else the login host.
@@ -2643,10 +2647,23 @@ export class FoundryStepExecutor {
2643
2647
  if (own) {
2644
2648
  // Written last: a failed base URI write leaves it behind and the step is planned again.
2645
2649
  context.recordScopeId(OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, ownCustomersSet.sort().join(','));
2650
+ if (swept)
2651
+ context.recordScopeId(LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION);
2646
2652
  }
2647
2653
  context.recordScopeId(LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, pendingRemovalState(stillPending));
2648
2654
  return { resourceId: host };
2649
2655
  }
2656
+ /**
2657
+ * `{ loginBaseUri }` for an app made in the org's ZITADEL: the instance's own login host (a company's own
2658
+ * `auth.` host, else the shared one). Nothing when the shared host is not known. See
2659
+ * {@link FoundryStepExecutorPorts}' `createOidcApp` `loginBaseUri`.
2660
+ */
2661
+ async instanceLoginBaseUriFor(org) {
2662
+ if (ownsServices(org))
2663
+ return { loginBaseUri: `https://${ownHostnames(org).auth}/` };
2664
+ const shared = await this.ports.platformOrg?.authHostname?.();
2665
+ return shared ? { loginBaseUri: `https://${shared}/` } : {};
2666
+ }
2650
2667
  /**
2651
2668
  * The host table of a company's own in-app login app as a bindings port that collects its edits and
2652
2669
  * writes them once on `flush` (one redeploy, none when nothing changed). Used for a company that runs
@@ -2931,6 +2948,7 @@ export class FoundryStepExecutor {
2931
2948
  : [...signInUris, ...(authEntry?.uses?.auth?.mcp ? [mcpCallbackUri(org)] : [])],
2932
2949
  postLogoutRedirectUris: postLogoutUris,
2933
2950
  ...(authEntry?.uses?.auth?.mcp ? { accessTokenType: 'jwt' } : {}),
2951
+ ...(await this.instanceLoginBaseUriFor(org)),
2934
2952
  });
2935
2953
  if (appType === 'web') {
2936
2954
  // `coolifyApp`'s own `pushAppEnv` reads `APP_CLIENT_ID_SCOPE_KEY` to
@@ -3869,6 +3887,7 @@ export class FoundryStepExecutor {
3869
3887
  appType: 'web',
3870
3888
  redirectUris: spec.redirectUris,
3871
3889
  postLogoutRedirectUris: spec.postLogoutRedirectUris,
3890
+ ...(await this.instanceLoginBaseUriFor(org)),
3872
3891
  });
3873
3892
  },
3874
3893
  };
@@ -3921,6 +3940,7 @@ export class FoundryStepExecutor {
3921
3940
  appType: 'web',
3922
3941
  redirectUris: spec.redirectUris,
3923
3942
  postLogoutRedirectUris: spec.postLogoutRedirectUris,
3943
+ ...(await this.instanceLoginBaseUriFor(org)),
3924
3944
  });
3925
3945
  },
3926
3946
  writeBrokerEntry: async (entryWant, appKey) => {
@@ -5203,13 +5223,28 @@ export class FoundryStepExecutor {
5203
5223
  return already ? null : randomBytes(bytes).toString('base64url');
5204
5224
  }
5205
5225
  /**
5206
- * Every company's Boule, F3/#970: `NATIVE_BUILD_KEY` is what Boule presents to Archon's
5207
- * native-build route (estate#262) to ask it to dispatch `build-app.yml`. Minted once, like
5208
- * {@link ensureAuthAppKey}, and never rotated; a present-but-empty value (the bug
5209
- * `mintNativeBuildKey`'s own read-then-generate had, see #957) is treated as not set.
5226
+ * foundry#987: nothing reads `NATIVE_BUILD_KEY` any more -- Boule calls Archon's org build
5227
+ * route (`/api/orgs/{orgId}/native/builds`) with its own org credential instead
5228
+ * (`BOULE_ARCHON_CREDENTIAL_ENV`, archon#706, boule#432). This takes the env row away from
5229
+ * every Boule that still has one from #970/#971; `NATIVE_BUILD_KEY_ENV` and
5230
+ * `NATIVE_BUILD_KEY_BYTES` are deprecated and go in the next release (the two-release removal
5231
+ * rule), once no live Boule holds the row any more.
5210
5232
  */
5211
- async ensureNativeBuildKey(appId) {
5212
- return this.mintIfAbsent(appId, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, 1);
5233
+ async dropNativeBuildKeyEnv(appId, liveRows) {
5234
+ let changed = false;
5235
+ for (const row of liveRows) {
5236
+ if (row.key !== NATIVE_BUILD_KEY_ENV || !row.uuid)
5237
+ continue;
5238
+ try {
5239
+ await this.ports.coolify.deleteEnv(appId, row.uuid);
5240
+ changed = true;
5241
+ }
5242
+ catch (error) {
5243
+ if (!isNotFoundError(error))
5244
+ throw error;
5245
+ }
5246
+ }
5247
+ return changed;
5213
5248
  }
5214
5249
  /**
5215
5250
  * `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
@@ -7703,12 +7738,10 @@ export class FoundryStepExecutor {
7703
7738
  this.handledSecrets.add(ownerSeedSecret);
7704
7739
  envs.push({ key: BOULE_OWNER_SEED_SECRET_ENV, value: ownerSeedSecret });
7705
7740
  }
7706
- // The key Boule presents to Archon's native-build route, for every org's Boule (foundry#970).
7707
- const nativeBuildKey = await this.ensureNativeBuildKey(app.appId);
7708
- if (nativeBuildKey) {
7709
- this.handledSecrets.add(nativeBuildKey);
7710
- envs.push({ key: NATIVE_BUILD_KEY_ENV, value: nativeBuildKey });
7711
- }
7741
+ // foundry#987: Boule calls Archon's org build route with its own org credential now, so
7742
+ // nothing reads this any more -- take the row away from every Boule that still has one.
7743
+ if (await this.dropNativeBuildKeyEnv(app.appId, liveRows))
7744
+ envChanged = true;
7712
7745
  // Held, minted once (C3). Compared with the live value so a changed one restarts the app.
7713
7746
  const credentialEnvs = [];
7714
7747
  let archonPushedAt = null;
@@ -7769,7 +7802,6 @@ export class FoundryStepExecutor {
7769
7802
  peerPassword,
7770
7803
  bridgeSecret,
7771
7804
  ownerSeedSecret,
7772
- nativeBuildKey,
7773
7805
  ...credentialEnvs.map((credential) => credential.value),
7774
7806
  ...grants.secrets,
7775
7807
  ].filter((v) => v !== null);
@@ -7809,7 +7841,6 @@ export class FoundryStepExecutor {
7809
7841
  peerPassword !== null ||
7810
7842
  bridgeSecret !== null ||
7811
7843
  ownerSeedSecret !== null ||
7812
- nativeBuildKey !== null ||
7813
7844
  secretsNotRestarted ||
7814
7845
  registrarUnstarted ||
7815
7846
  dbPush.changed ||
@@ -11448,6 +11479,46 @@ export function isNoChangesError(error) {
11448
11479
  * {@link ZitadelPort.updateOidcAppLoginBaseUri} that counts an app already holding the wanted base URI as
11449
11480
  * done: ZITADEL answers that write with "No changes" and a repeat apply must not fail on it.
11450
11481
  */
11482
+ /**
11483
+ * Turns the instance feature `loginV2.required` off so each app's own base URI counts, and makes sure no
11484
+ * app can then land on ZITADEL's v1 login: with `required` off an app without a base URI of its own
11485
+ * (ZITADEL's own console app, an app foundry did not make, an app made later) falls back to v1, and
11486
+ * ZITADEL v4.17.3 gives a `required` instance's base URI to every app but ignores an app's own
11487
+ * (`internal/query/oidc_client.go:88-92`), so the only way is a base URI on every app. Every live OIDC
11488
+ * app of every organisation and project gets `instanceBaseUri` when it has none; an app that has one
11489
+ * keeps it. The apps are set first, while `required` still makes the instance ignore them, so no sign-in
11490
+ * passes through a gap. Without a way to list every app, or with a listing that may be cut short, it
11491
+ * throws before the feature is touched.
11492
+ */
11493
+ export async function allowPerAppLoginBaseUris(zitadel, instanceBaseUri) {
11494
+ if (!zitadel.allowPerAppLoginBaseUri)
11495
+ return;
11496
+ const { listOrgs, listProjects } = zitadel;
11497
+ if (!listOrgs || !listProjects) {
11498
+ throw new Error('the ZITADEL port cannot list every organisation and project, so apps without a login base URI would fall back to the v1 login');
11499
+ }
11500
+ for (const org of await listOrgs()) {
11501
+ if (org.state && org.state !== 'ORG_STATE_ACTIVE')
11502
+ continue;
11503
+ for (const project of await listProjects(org.id)) {
11504
+ if (project.state && project.state !== 'PROJECT_STATE_ACTIVE')
11505
+ continue;
11506
+ const listed = await zitadel.listApps(org.id, project.id);
11507
+ if (!listed.complete) {
11508
+ throw new Error(`the app listing of project ${project.id} may be cut short, so apps without a login base URI could be missed`);
11509
+ }
11510
+ for (const app of listed.apps) {
11511
+ if (app.state === 'APP_STATE_REMOVED' || app.appType === 'api')
11512
+ continue;
11513
+ // `undefined` is "not read": set it, the write is the same either way.
11514
+ if (typeof app.loginBaseUri === 'string' && app.loginBaseUri !== '')
11515
+ continue;
11516
+ await setOidcAppLoginBaseUri(zitadel, org.id, project.id, app.id, instanceBaseUri);
11517
+ }
11518
+ }
11519
+ }
11520
+ await zitadel.allowPerAppLoginBaseUri(instanceBaseUri);
11521
+ }
11451
11522
  export async function setOidcAppLoginBaseUri(zitadel, orgId, projectId, appId, loginBaseUri) {
11452
11523
  try {
11453
11524
  await zitadel.updateOidcAppLoginBaseUri(orgId, projectId, appId, loginBaseUri);
@@ -190,6 +190,14 @@ export declare function customersClientOrganizations(org: DeclaredOrg, recorded:
190
190
  * falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
191
191
  */
192
192
  export declare const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = "own_customers_login_base_uri";
193
+ /**
194
+ * Scope id recording that the instance's apps were all given a Login V2 base URI ({@link LOGIN_BASE_URI_SWEEP_VERSION}),
195
+ * on a company's own instance and on the platform's (Archon's step). A value that is missing or older plans the step
196
+ * again while in-app sign-in has `loginV2.required` off, so an instance applied before the sweep gets it too.
197
+ */
198
+ export declare const LOGIN_BASE_URI_SWEEP_SCOPE_KEY = "login_base_uri_sweep";
199
+ /** The value {@link LOGIN_BASE_URI_SWEEP_SCOPE_KEY} holds once the sweep ran. Raise it to run the sweep again everywhere. */
200
+ export declare const LOGIN_BASE_URI_SWEEP_VERSION = "1";
193
201
  /** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
194
202
  export declare function ownCustomersAppState(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): string;
195
203
  /** {@link LOGIN_CLIENT_ORGS_SCOPE_KEY}'s value for a map. */
@@ -223,6 +223,14 @@ export function customersClientOrganizations(org, recorded) {
223
223
  * falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
224
224
  */
225
225
  export const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = 'own_customers_login_base_uri';
226
+ /**
227
+ * Scope id recording that the instance's apps were all given a Login V2 base URI ({@link LOGIN_BASE_URI_SWEEP_VERSION}),
228
+ * on a company's own instance and on the platform's (Archon's step). A value that is missing or older plans the step
229
+ * again while in-app sign-in has `loginV2.required` off, so an instance applied before the sweep gets it too.
230
+ */
231
+ export const LOGIN_BASE_URI_SWEEP_SCOPE_KEY = 'login_base_uri_sweep';
232
+ /** The value {@link LOGIN_BASE_URI_SWEEP_SCOPE_KEY} holds once the sweep ran. Raise it to run the sweep again everywhere. */
233
+ export const LOGIN_BASE_URI_SWEEP_VERSION = '1';
226
234
  /** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
227
235
  export function ownCustomersAppState(org, recorded) {
228
236
  return [...customersEntryKeys(org)]
@@ -585,6 +585,8 @@ export interface FoundryPlanApplyOptions {
585
585
  keysRegistrarUrl?: string;
586
586
  /** C2: true when the host's executor was built with a `WorkerAppDatabasePort`. */
587
587
  workerAppDatabase?: boolean;
588
+ /** True when that port has `deletableTables`. A `workerApp.database.noDelete` is blocked without it. */
589
+ workerAppNoDeleteCheck?: boolean;
588
590
  /** ADR 0002: the platform's Cloudflare account id. A container image outside its registry is blocked. */
589
591
  workerAppRegistryAccountId?: string;
590
592
  /**
@@ -746,6 +748,7 @@ export declare class FoundryPlanApply implements PlanApply {
746
748
  private readonly workerAppKeysUrl;
747
749
  private readonly keysRegistrarUrl;
748
750
  private readonly workerAppDatabase;
751
+ private readonly workerAppNoDeleteCheck;
749
752
  private readonly workerAppRegistryAccountId;
750
753
  private readonly ownerInviteMail;
751
754
  private readonly hostFiles;
@@ -980,6 +983,12 @@ export declare class FoundryPlanApply implements PlanApply {
980
983
  private repoKeysReadiness;
981
984
  /** Which refs of the platform GitHub App hold no value now. A resolver that throws counts as not held; only refs are kept. */
982
985
  private githubAppKeysReadiness;
986
+ /**
987
+ * The Worker apps' `heldSecret` secrets that hold a value now, by `<entry key>/<NAME>`, with the version of
988
+ * the value. A resolver that throws, or a name that fits no ref, counts as not held. The value is hashed and
989
+ * dropped here; only the version leaves.
990
+ */
991
+ private workerAppSecretsReadiness;
983
992
  /**
984
993
  * The org's `heldSecret` envs: the refs that hold a value now, which of those differ from the live
985
994
  * production env, which could not be read, and which fit no ref. A resolver that throws counts as
@@ -40,7 +40,7 @@ import { CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY, CLOUDFLARE_TUNNEL_ENTRY_ROU
40
40
  import { finishStageCopy, runSubjectRelease, } from './release-host.js';
41
41
  import { remoteDatabaseFactsFrom, remoteDatabaseKeys } from './remote-database.js';
42
42
  import { STAGE_COPY_LOCK_MS, claimStageCopyLock, parseStageCopyLock, stageCopyLockValue, } from './stage-copy-host.js';
43
- import { liveWorkerApps, workerAppTraceScopeKeys } from './worker-app.js';
43
+ import { heldSecretId, heldSecretVersion, liveWorkerApps, workerAppTraceScopeKeys, } from './worker-app.js';
44
44
  import { liveWorkers, workerTraceScopeKeys } from './worker.js';
45
45
  /** #441: the lease scope that serializes every mutation of one company, and the one for the shared platform. */
46
46
  const companyScope = (orgId) => `company:${orgId}`;
@@ -189,6 +189,7 @@ export class FoundryPlanApply {
189
189
  workerAppKeysUrl;
190
190
  keysRegistrarUrl;
191
191
  workerAppDatabase;
192
+ workerAppNoDeleteCheck;
192
193
  workerAppRegistryAccountId;
193
194
  ownerInviteMail;
194
195
  hostFiles;
@@ -233,6 +234,7 @@ export class FoundryPlanApply {
233
234
  this.workerAppKeysUrl = options.workerAppKeysUrl;
234
235
  this.keysRegistrarUrl = options.keysRegistrarUrl;
235
236
  this.workerAppDatabase = options.workerAppDatabase ?? false;
237
+ this.workerAppNoDeleteCheck = options.workerAppNoDeleteCheck ?? false;
236
238
  this.workerAppRegistryAccountId = options.workerAppRegistryAccountId;
237
239
  this.ownerInviteMail = options.ownerInviteMail ?? false;
238
240
  this.hostFiles = options.filesOrganisation ?? false;
@@ -1626,6 +1628,7 @@ export class FoundryPlanApply {
1626
1628
  workerAppKeysUrl: this.workerAppKeysUrl,
1627
1629
  keysRegistrarUrl: this.keysRegistrarUrl,
1628
1630
  workerAppDatabase: this.workerAppDatabase,
1631
+ workerAppNoDeleteCheck: this.workerAppNoDeleteCheck,
1629
1632
  workerAppRegistryAccountId: this.workerAppRegistryAccountId,
1630
1633
  ownerInviteMail: this.ownerInviteMail,
1631
1634
  otherSlugs,
@@ -1758,6 +1761,7 @@ export class FoundryPlanApply {
1758
1761
  const base = {
1759
1762
  ...(await this.sharedPlatformReadiness()),
1760
1763
  heldEnv: await this.heldEnvReadiness(org),
1764
+ workerAppSecrets: await this.workerAppSecretsReadiness(org),
1761
1765
  ...(org.hosting.github ? { githubAppKeys: await this.githubAppKeysReadiness() } : {}),
1762
1766
  ...(repoKeysInPlay(org, orgRecordedIds)
1763
1767
  ? { repoKeys: await this.repoKeysReadiness(org, orgRecordedIds) }
@@ -1805,6 +1809,32 @@ export class FoundryPlanApply {
1805
1809
  }
1806
1810
  return { store: true, missing };
1807
1811
  }
1812
+ /**
1813
+ * The Worker apps' `heldSecret` secrets that hold a value now, by `<entry key>/<NAME>`, with the version of
1814
+ * the value. A resolver that throws, or a name that fits no ref, counts as not held. The value is hashed and
1815
+ * dropped here; only the version leaves.
1816
+ */
1817
+ async workerAppSecretsReadiness(org) {
1818
+ const heldKeys = this.platformDeps?.heldKeys;
1819
+ const held = {};
1820
+ if (!heldKeys)
1821
+ return held;
1822
+ for (const entry of org.hosting.entries) {
1823
+ for (const [name, source] of Object.entries(entry.workerApp?.secrets ?? {})) {
1824
+ if (source.from !== 'heldSecret')
1825
+ continue;
1826
+ try {
1827
+ const value = await heldKeys.resolve(heldEnvRef(org.id, entry.key, name));
1828
+ if (value)
1829
+ held[heldSecretId(entry.key, name)] = heldSecretVersion(value);
1830
+ }
1831
+ catch {
1832
+ // Not held: the plan says the secret has no value yet, and the error is dropped.
1833
+ }
1834
+ }
1835
+ }
1836
+ return held;
1837
+ }
1808
1838
  /**
1809
1839
  * The org's `heldSecret` envs: the refs that hold a value now, which of those differ from the live
1810
1840
  * production env, which could not be read, and which fit no ref. A resolver that throws counts as
@@ -8,6 +8,7 @@ import { buildDriftNotes } from './build-record.js';
8
8
  import { versionChangeNeedsConfirmation } from './confirm-upgrade.js';
9
9
  import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
10
10
  import { legacyAppHostname } from './hostnames.js';
11
+ import { LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION } from './login-host.js';
11
12
  import { PLATFORM_MAIL_RELAY_KEY, planMailRelay } from './mail-relay.js';
12
13
  import { MAIL_DATABASE_SCOPE_KEY, mailStoreIsLegacy, mailStoreSwitchChange, mailStoreSwitchDescription, } from './mail-store.js';
13
14
  import { planOikosIngress } from './oikos-ingress.js';
@@ -3482,6 +3483,10 @@ function planArchon(platform, recordedIds, executorBlocked, ownerSignIn, ownerFi
3482
3483
  // In-app sign-in: Archon's own address is the sign-in address only once the owner company has the
3483
3484
  // switch on and its in-app login is applied, and goes back to the company's login host when it does not.
3484
3485
  archonInAppHost(recordedIds) === (ownerSignIn.signInInApp ? platform.archon.hostname : '') &&
3486
+ // With in-app sign-in on, `loginV2.required` is off and every app of the instance owes a base URI
3487
+ // until the sweep ran (an instance applied before it plans the step once more).
3488
+ (!ownerSignIn.signInInApp ||
3489
+ recordedIds[LOGIN_BASE_URI_SWEEP_SCOPE_KEY] === LOGIN_BASE_URI_SWEEP_VERSION) &&
3485
3490
  recordedIds[PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY] === archonOrgId &&
3486
3491
  !recordedIds[PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY] &&
3487
3492
  Boolean(recordedIds[PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY]) &&
@@ -194,6 +194,12 @@ export interface OrgPlatformReadiness {
194
194
  unreadable?: readonly string[];
195
195
  invalid?: readonly string[];
196
196
  };
197
+ /**
198
+ * The Worker apps' `heldSecret` secrets that hold a value now: `<entry key>/<NAME>` -> the version of that
199
+ * value (a hash, `heldSecretVersion`; never the value). A secret with no entry here is unset. Absent reads as
200
+ * none held.
201
+ */
202
+ workerAppSecrets?: Readonly<Record<string, string>>;
197
203
  /**
198
204
  * The platform GitHub App's held keys (`PLATFORM_GITHUB_APP_REFS`), read only for a company that declares
199
205
  * `hosting.github`. `store` is false when the host wired no held-keys reader; `missing` are the refs that hold
@@ -542,13 +542,15 @@ export declare const PLATFORM_AUTH_LOGIN_INTERNAL_NAME = "platform-auth-login";
542
542
  export declare function zitadelDockerfile(): string;
543
543
  export declare const AUTH_LOGIN_PORT = "3000";
544
544
  /**
545
- * Every ZITADEL OIDC/management/console/asset path -- `deploy/proxy/Caddyfile`'s
546
- * own explicit list, restated here as one regex since the tunnel routes by a
545
+ * Every ZITADEL OIDC/management/asset path -- `deploy/proxy/Caddyfile`'s own
546
+ * explicit list, restated here as one regex since the tunnel routes by a
547
547
  * single `path` per ingress rule rather than a set. Anything not matching
548
548
  * falls through to the login app's own ingress rule below (login v2 answers
549
- * everything else, including the root).
549
+ * everything else, including the root). `/ui/console` and `/ui/login/*` (the
550
+ * v1 login) are left out on purpose: nobody reaches ZITADEL's own screens, so
551
+ * they land on the login app, which sends them back to the app.
550
552
  */
551
- export declare const AUTH_ZITADEL_PATH_PATTERN = "^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|ui/console|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)";
553
+ export declare const AUTH_ZITADEL_PATH_PATTERN = "^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)";
552
554
  /**
553
555
  * The broker's MCP authorization server (auth ADR 0002): its metadata and `/mcp-oauth/*`.
554
556
  * Checked before {@link AUTH_ZITADEL_PATH_PATTERN}, which takes every other `/.well-known` path.
@@ -1000,7 +1002,7 @@ export interface PlatformArchonDeps {
1000
1002
  releasePreviewMembers?: ReleasePreviewMembers;
1001
1003
  cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
1002
1004
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
1003
- zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'> & Partial<Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'allowPerAppLoginBaseUri' | 'updateOidcAppPostLogoutRedirectUris' | 'getOidcAppRedirectUris' | 'updateOidcAppRedirectUris'>>;
1005
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'> & Partial<Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'allowPerAppLoginBaseUri' | 'listOrgs' | 'listProjects' | 'updateOidcAppPostLogoutRedirectUris' | 'getOidcAppRedirectUris' | 'updateOidcAppRedirectUris'>>;
1004
1006
  /**
1005
1007
  * {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client:
1006
1008
  * the owner company's own ZITADEL, by its issuer and the held-key reference of
@@ -1641,7 +1643,7 @@ export interface PlatformAccessProviderDeps {
1641
1643
  /** Edge's Access calls, at its recorded address (see {@link PlatformAccessFactory}). */
1642
1644
  access: (edgeUrl: string) => PlatformAccessProviderPort | Promise<PlatformAccessProviderPort>;
1643
1645
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
1644
- zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'listApps' | 'deleteApp'> & Required<Pick<ZitadelPort, 'createOidcAppWithSecret'>>;
1646
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'listApps' | 'deleteApp'> & Required<Pick<ZitadelPort, 'createOidcAppWithSecret'>> & Partial<Pick<ZitadelPort, 'updateOidcAppLoginBaseUri'>>;
1645
1647
  /** {@link PlatformArchonDeps.zitadelForInstance}: the ZITADEL client for the sign-in at `issuerUrl`. */
1646
1648
  zitadelForInstance?: (issuerUrl: string, bearerRef?: string) => PlatformAccessProviderDeps['zitadel'];
1647
1649
  /** Sleep and delivery tries for the one call that carries the secret; real ones by default. */
@@ -13,7 +13,7 @@ import { previewsOn } from './coolify-previews.js';
13
13
  import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
14
14
  import { desktopCallbackUri, unionRedirectUris } from './desktop-callback.js';
15
15
  import { placedOnPlatform } from './destinations.js';
16
- import { ACCESS_POLICY_NAME, LOG_DIR, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
16
+ import { ACCESS_POLICY_NAME, LOG_DIR, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, setOidcAppLoginBaseUri, } from './executor.js';
17
17
  import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
18
18
  import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, tcpProbe, } from './health-check-settings.js';
19
19
  import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
@@ -27,6 +27,7 @@ import { BROKER_DB_NAME, BROKER_DB_USER, ensureBrokerMcpEnv } from './broker-mcp
27
27
  import { withTimeout } from './call-timeout.js';
28
28
  import { SIGN_IN_BIND_KEY_ENV, SIGN_IN_BIND_KEY_MIN_LENGTH, SIGN_IN_BIND_KEY_SCOPE_KEY, } from './cookie-domain.js';
29
29
  import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
30
+ import { LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION } from './login-host.js';
30
31
  import { MAIL_RELAY_BLOCKED, provisionMailRelay } from './mail-relay.js';
31
32
  import { MAIL_APP_SCOPE_KEY, MAIL_DATABASE_NAME, MAIL_DATABASE_PASSWORD_ENV, MAIL_DATABASE_SCOPE_KEY, MAIL_STORE_POSTGRES, MAIL_STORE_SCOPE_KEY, mailStoreIsLegacy, } from './mail-store.js';
32
33
  import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
@@ -1473,17 +1474,18 @@ export const AUTH_LOGIN_PORT = '3000';
1473
1474
  /** `broker/Dockerfile`'s own `ENV PORT=3000` / `EXPOSE 3000`. */
1474
1475
  const AUTH_BROKER_PORT = '3000';
1475
1476
  /**
1476
- * Every ZITADEL OIDC/management/console/asset path -- `deploy/proxy/Caddyfile`'s
1477
- * own explicit list, restated here as one regex since the tunnel routes by a
1477
+ * Every ZITADEL OIDC/management/asset path -- `deploy/proxy/Caddyfile`'s own
1478
+ * explicit list, restated here as one regex since the tunnel routes by a
1478
1479
  * single `path` per ingress rule rather than a set. Anything not matching
1479
1480
  * falls through to the login app's own ingress rule below (login v2 answers
1480
- * everything else, including the root).
1481
+ * everything else, including the root). `/ui/console` and `/ui/login/*` (the
1482
+ * v1 login) are left out on purpose: nobody reaches ZITADEL's own screens, so
1483
+ * they land on the login app, which sends them back to the app.
1481
1484
  */
1482
1485
  // `v2`, `v2beta` and `zitadel.<service>` are ZITADEL's v2 REST and connect
1483
1486
  // APIs (providers' `inviteOrgOwner` calls `POST /v2/users`); without them those
1484
- // calls reach the login app and answer 404. The login app itself lives under
1485
- // `/ui/v2/login`, which none of these match.
1486
- export const AUTH_ZITADEL_PATH_PATTERN = '^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|ui/console|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)';
1487
+ // calls reach the login app and answer 404. None of these match `/ui/*`.
1488
+ export const AUTH_ZITADEL_PATH_PATTERN = '^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)';
1487
1489
  /**
1488
1490
  * `@wtfalch/auth`'s own `brokerUrl` default, `<issuer>/api`
1489
1491
  * (`packages/auth/src/config.ts`) -- `deploy/proxy/Caddyfile`'s own
@@ -4671,6 +4673,8 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4671
4673
  // `docs/operator-deployment.md`, step 4).
4672
4674
  redirectUris: [`https://${hostname}/auth/callback`, desktopCallbackUri(hostname)],
4673
4675
  postLogoutRedirectUris: postLogoutUrisFor([hostname]),
4676
+ // The owner company's login host: with `loginV2.required` off a client with none lands on the v1 login.
4677
+ loginBaseUri: `https://${ownerSignIn.hostname}/`,
4674
4678
  });
4675
4679
  context.recordScopeId(PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, created.appId);
4676
4680
  context.recordScopeId(PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, postLogoutAppliedValue(created.appId, postLogoutUrisFor([hostname])));
@@ -4693,7 +4697,7 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4693
4697
  const archonAppZitadelId = context.recordedIds()[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
4694
4698
  const inAppOwed = archonInAppHost(context.recordedIds()) !== '';
4695
4699
  const inAppCalls = () => {
4696
- const { ensureTrustedDomain, deleteTrustedDomain, updateOidcAppLoginBaseUri, allowPerAppLoginBaseUri, } = zitadel;
4700
+ const { ensureTrustedDomain, deleteTrustedDomain, updateOidcAppLoginBaseUri, allowPerAppLoginBaseUri, listOrgs, listProjects, } = zitadel;
4697
4701
  if (!ensureTrustedDomain || !deleteTrustedDomain || !updateOidcAppLoginBaseUri) {
4698
4702
  throw new Error("platform:archon: the owner company's ZITADEL port lacks ensureTrustedDomain, deleteTrustedDomain or updateOidcAppLoginBaseUri, so Archon cannot sign in on its own address");
4699
4703
  }
@@ -4701,9 +4705,12 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4701
4705
  ensureTrustedDomain: ensureTrustedDomain.bind(zitadel),
4702
4706
  deleteTrustedDomain: deleteTrustedDomain.bind(zitadel),
4703
4707
  updateOidcAppLoginBaseUri: updateOidcAppLoginBaseUri.bind(zitadel),
4708
+ listApps: zitadel.listApps.bind(zitadel),
4704
4709
  ...(allowPerAppLoginBaseUri
4705
4710
  ? { allowPerAppLoginBaseUri: allowPerAppLoginBaseUri.bind(zitadel) }
4706
4711
  : {}),
4712
+ ...(listOrgs ? { listOrgs: listOrgs.bind(zitadel) } : {}),
4713
+ ...(listProjects ? { listProjects: listProjects.bind(zitadel) } : {}),
4707
4714
  };
4708
4715
  };
4709
4716
  const inAppTarget = {
@@ -5217,6 +5224,7 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
5217
5224
  });
5218
5225
  }
5219
5226
  await record(PLATFORM_ARCHON_IN_APP_SCOPE_KEY, `${hostname}=${clientId}`);
5227
+ await record(LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION);
5220
5228
  }
5221
5229
  else if (inAppOwed) {
5222
5230
  if (!ownerSignIn.signInLoginAppId) {
@@ -8320,6 +8328,11 @@ async function provisionPlatformAccessProvider(context, deps) {
8320
8328
  context.recordScopeId(PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, client.appId);
8321
8329
  context.recordScopeId(PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, client.appId);
8322
8330
  await context.persist?.();
8331
+ // The owner company's login host, so the client never lands on the v1 login when `loginV2.required` is off.
8332
+ // After the ids are persisted: a failure here leaves the app recorded and costs no second secret.
8333
+ if (zitadel.updateOidcAppLoginBaseUri) {
8334
+ await setOidcAppLoginBaseUri({ updateOidcAppLoginBaseUri: zitadel.updateOidcAppLoginBaseUri.bind(zitadel) }, org.id, project.id, client.appId, `https://${ownerSignIn.hostname}/`);
8335
+ }
8323
8336
  let providerId = '';
8324
8337
  await deliverWithRetry(async (_ref, secret) => {
8325
8338
  providerId = (await access.ensureProvider({