@wtfalch/foundry 0.64.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.
- package/dist/apps/cloudflare.d.ts +5 -0
- package/dist/apps/deploy.js +33 -1
- package/dist/apps/migrations.d.ts +3 -0
- package/dist/apps/migrations.js +32 -7
- package/dist/entry-rules.d.ts +11 -0
- package/dist/entry-rules.js +70 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- package/dist/migrations/0069_release_events.sql +16 -0
- package/dist/modules/database.js +22 -3
- package/dist/native-build.d.ts +16 -12
- package/dist/native-build.js +16 -12
- package/dist/plan/boule.js +2 -0
- package/dist/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +17 -3
- package/dist/plan/executor.d.ts +44 -7
- package/dist/plan/executor.js +89 -18
- package/dist/plan/login-host.d.ts +8 -0
- package/dist/plan/login-host.js +8 -0
- package/dist/plan/plan-apply.d.ts +9 -0
- package/dist/plan/plan-apply.js +31 -1
- package/dist/plan/platform-diff.js +5 -0
- package/dist/plan/platform-org.d.ts +6 -0
- package/dist/plan/platform-providers.d.ts +8 -6
- package/dist/plan/platform-providers.js +22 -9
- package/dist/plan/release-host.d.ts +30 -1
- package/dist/plan/release-host.js +84 -41
- package/dist/plan/release-series-reader.d.ts +26 -0
- package/dist/plan/release-series-reader.js +40 -0
- package/dist/plan/sign-in-archon.d.ts +1 -1
- package/dist/plan/sign-in-archon.js +2 -2
- package/dist/plan/worker-app-database.d.ts +12 -1
- package/dist/plan/worker-app-database.js +13 -0
- package/dist/plan/worker-app.d.ts +27 -2
- package/dist/plan/worker-app.js +52 -7
- package/dist/plan/workspace.d.ts +2 -2
- package/dist/plan/workspace.js +3 -3
- package/dist/release/decide.d.ts +2 -0
- package/dist/release/decide.js +67 -10
- package/dist/release/events.d.ts +13 -0
- package/dist/release/events.js +67 -0
- package/dist/release/health.d.ts +5 -0
- package/dist/release/health.js +3 -3
- package/dist/release/run.d.ts +7 -2
- package/dist/release/run.js +14 -1
- package/dist/release/series.d.ts +58 -0
- package/dist/release/series.js +65 -0
- package/dist/release/state.d.ts +67 -1
- package/dist/release/state.js +69 -0
- package/dist/release/types.d.ts +52 -0
- package/dist/store/postgres.js +29 -3
- package/dist/store/schema.d.ts +112 -0
- package/dist/store/schema.js +9 -0
- package/dist/worker-app-config.d.ts +22 -1
- package/dist/worker-app-config.js +71 -13
- package/dist/worker-app-migrations.d.ts +6 -2
- package/dist/worker-app-migrations.js +1 -0
- package/package.json +1 -1
package/dist/plan/executor.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -1765,7 +1782,7 @@ export interface FoundryStepExecutorPorts {
|
|
|
1765
1782
|
/**
|
|
1766
1783
|
* The stage copy (`copy_prod_to_stage`, run by the release tick). The host supplies the dump download and the
|
|
1767
1784
|
* backup storage; foundry builds the restore port itself and reads production's address live from Coolify.
|
|
1768
|
-
* Without it a copy request is refused with `no-copy-port`. The host needs `pg_restore`
|
|
1785
|
+
* Without it a copy request is refused with `no-copy-port`. The host needs `pg_restore` on PATH, of the production database's Postgres version or newer (platform-archon-db runs 18).
|
|
1769
1786
|
*/
|
|
1770
1787
|
stageCopy?: StageCopyHostConfig;
|
|
1771
1788
|
/**
|
|
@@ -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
|
-
*
|
|
2780
|
-
*
|
|
2781
|
-
*
|
|
2782
|
-
*
|
|
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
|
|
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>;
|
package/dist/plan/executor.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
2432
|
-
|
|
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
|
-
*
|
|
5207
|
-
*
|
|
5208
|
-
*
|
|
5209
|
-
*
|
|
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
|
|
5212
|
-
|
|
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
|
-
//
|
|
7707
|
-
|
|
7708
|
-
if (
|
|
7709
|
-
|
|
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. */
|
package/dist/plan/login-host.js
CHANGED
|
@@ -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
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -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/
|
|
546
|
-
*
|
|
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
|
|
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/
|
|
1477
|
-
*
|
|
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.
|
|
1485
|
-
|
|
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) {
|
|
@@ -6237,7 +6245,7 @@ function assertSameDatabaseHost(key, liveUrl, dbInternalUrl) {
|
|
|
6237
6245
|
/**
|
|
6238
6246
|
* Throws when `liveUrl`, kept as it is on the app, names another database than
|
|
6239
6247
|
* {@link ARCHON_POSTGRES_DB}: a hand-set URL on the wrong path, or a platform
|
|
6240
|
-
* still on
|
|
6248
|
+
* still on a retired database. Names the two databases only, never the URL.
|
|
6241
6249
|
*/
|
|
6242
6250
|
function assertArchonDatabasePath(key, liveUrl) {
|
|
6243
6251
|
let database;
|
|
@@ -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({
|