@wtfalch/foundry 0.41.1 → 0.42.1
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/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0054_org_signin_in_app.sql +6 -0
- package/dist/org.d.ts +17 -0
- package/dist/org.js +14 -0
- package/dist/plan/agora.d.ts +4 -1
- package/dist/plan/agora.js +16 -2
- package/dist/plan/boule.d.ts +5 -2
- package/dist/plan/boule.js +14 -2
- package/dist/plan/coolify-previews.d.ts +10 -0
- package/dist/plan/coolify-previews.js +14 -0
- package/dist/plan/diff.js +92 -9
- package/dist/plan/edge-ratelimits.d.ts +5 -0
- package/dist/plan/edge-ratelimits.js +16 -6
- package/dist/plan/executor.d.ts +45 -0
- package/dist/plan/executor.js +283 -22
- package/dist/plan/login-host.d.ts +23 -2
- package/dist/plan/login-host.js +30 -5
- package/dist/plan/own-diff.d.ts +1 -1
- package/dist/plan/own-diff.js +9 -2
- package/dist/plan/own-sfu.d.ts +2 -0
- package/dist/plan/own-sfu.js +3 -0
- package/dist/plan/platform-diff.js +4 -0
- package/dist/plan/platform-org.d.ts +6 -0
- package/dist/plan/platform-org.js +5 -0
- package/dist/plan/platform-providers.d.ts +16 -1
- package/dist/plan/platform-providers.js +255 -8
- package/dist/plan/providers-readers.js +6 -2
- package/dist/plan/sign-in-archon.d.ts +46 -0
- package/dist/plan/sign-in-archon.js +71 -0
- package/dist/plan/sign-in-bindings.d.ts +16 -0
- package/dist/plan/sign-in-bindings.js +44 -0
- package/dist/plan/sign-in.d.ts +135 -0
- package/dist/plan/sign-in.js +172 -0
- package/dist/store/postgres.js +7 -1
- package/dist/store/schema.d.ts +17 -0
- package/dist/store/schema.js +2 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceSchema, ServicesConfig, VersionPin, VersionPinPart, } from './org.js';
|
|
2
|
-
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, VERSION_PIN_PARTS, appEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateVersionPin, validateSite, } from './org.js';
|
|
1
|
+
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceSchema, ServicesConfig, SignInConfig, VersionPin, VersionPinPart, } from './org.js';
|
|
2
|
+
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, VERSION_PIN_PARTS, appEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateSignIn, validateVersionPin, validateSite, } from './org.js';
|
|
3
3
|
export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, HeartbeatsProvider, } from './platform.js';
|
|
4
4
|
export { PLATFORM_SERVICE_KEYS, HEARTBEATS_PROVIDERS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
5
5
|
import { type PinnedVersions } from './bootstrap/versions.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, VERSION_PIN_PARTS, appEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateVersionPin, validateSite, } from './org.js';
|
|
1
|
+
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, VERSION_PIN_PARTS, appEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateSignIn, validateVersionPin, validateSite, } from './org.js';
|
|
2
2
|
export { PLATFORM_SERVICE_KEYS, HEARTBEATS_PROVIDERS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
3
3
|
// archon#273: the pinned platform service versions the planner deploys, so a
|
|
4
4
|
// host can show declared against running. Readonly: it is the planner's own
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- @wtfalch/foundry: a company's in-app sign-in switch. `signin_in_app` true moves the
|
|
2
|
+
-- people's sign-in onto each app's own address (`/sign-in`); false (default) keeps the
|
|
3
|
+
-- company's login host. Additive. Mirrors src/store/schema.ts's `declaredOrgs`.
|
|
4
|
+
|
|
5
|
+
alter table foundry_declared_orgs
|
|
6
|
+
add column if not exists signin_in_app boolean not null default false;
|
package/dist/org.d.ts
CHANGED
|
@@ -636,6 +636,21 @@ export interface ServicesConfig {
|
|
|
636
636
|
sfuSlot?: number | null;
|
|
637
637
|
}
|
|
638
638
|
export declare const SFU_SLOT_MAX = 20;
|
|
639
|
+
/**
|
|
640
|
+
* Per-company sign-in placement. `inApp` (default off) moves the people's sign-in from the
|
|
641
|
+
* company's login host onto each app's own address (`/sign-in` on agora.<domain>, boule.<domain>),
|
|
642
|
+
* through the login that serves that path. Read through `plan/sign-in.ts`'s `inAppSignIn`.
|
|
643
|
+
*/
|
|
644
|
+
export interface SignInConfig {
|
|
645
|
+
inApp: boolean;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* `null` when `signIn` is valid. The switch needs a company domain whose login host is
|
|
649
|
+
* `auth.<domain>`: a company on `oikos.host` (tier 1) keeps today's flow, so the declaration
|
|
650
|
+
* is refused there rather than silently ignored. A company with its own services is covered: its
|
|
651
|
+
* own sign-in stack gets the second login app.
|
|
652
|
+
*/
|
|
653
|
+
export declare function validateSignIn(org: Pick<DeclaredOrg, 'signIn' | 'domain' | 'issuer' | 'services'>): string | null;
|
|
639
654
|
/** The only reader of `services.own`. Every company runs its own services, the platform's owner company too; a company without a domain runs them on oikos.host. */
|
|
640
655
|
export declare function ownsServices(org: Pick<DeclaredOrg, 'services'>): boolean;
|
|
641
656
|
/** The pure part of the `services` rules; that no two orgs share an `sfuSlot` is the store's check. */
|
|
@@ -916,6 +931,8 @@ export interface DeclaredOrg {
|
|
|
916
931
|
hosting: HostingConfig;
|
|
917
932
|
/** Absent means `{ own: false }`. Optional so every existing literal and test fixture compiles unchanged, as `calls.versionPin` and `MailConfig.mailApps` do. */
|
|
918
933
|
services?: ServicesConfig;
|
|
934
|
+
/** Absent means `{ inApp: false }`. Optional so every existing literal and fixture compiles unchanged. */
|
|
935
|
+
signIn?: SignInConfig;
|
|
919
936
|
/** Parts of this company held on their own version. Absent or null follows the platform. Ignored for the owner company. */
|
|
920
937
|
versionPin?: VersionPin | null;
|
|
921
938
|
sso: SsoConfig;
|
package/dist/org.js
CHANGED
|
@@ -904,6 +904,20 @@ export function validateVersionPin(org) {
|
|
|
904
904
|
return null;
|
|
905
905
|
}
|
|
906
906
|
export const SFU_SLOT_MAX = 20;
|
|
907
|
+
/**
|
|
908
|
+
* `null` when `signIn` is valid. The switch needs a company domain whose login host is
|
|
909
|
+
* `auth.<domain>`: a company on `oikos.host` (tier 1) keeps today's flow, so the declaration
|
|
910
|
+
* is refused there rather than silently ignored. A company with its own services is covered: its
|
|
911
|
+
* own sign-in stack gets the second login app.
|
|
912
|
+
*/
|
|
913
|
+
export function validateSignIn(org) {
|
|
914
|
+
if (org.signIn?.inApp !== true)
|
|
915
|
+
return null;
|
|
916
|
+
if (!org.domain || !org.issuer.usesDomain) {
|
|
917
|
+
return 'signIn.inApp needs a company domain with issuer.usesDomain: a company on oikos.host keeps the sign-in on its login host';
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
907
921
|
/** The only reader of `services.own`. Every company runs its own services, the platform's owner company too; a company without a domain runs them on oikos.host. */
|
|
908
922
|
export function ownsServices(org) {
|
|
909
923
|
return org.services?.own === true;
|
package/dist/plan/agora.d.ts
CHANGED
|
@@ -224,7 +224,10 @@ export declare function desiredAgoraEnv(org: DeclaredOrg, recorded: Readonly<Rec
|
|
|
224
224
|
env: Record<string, string>;
|
|
225
225
|
missing: string[];
|
|
226
226
|
};
|
|
227
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* The desired env keys the live app lacks or holds a different value for. Extra live keys are ignored,
|
|
229
|
+
* except `SIGNIN_UPSTREAM`: the switch turned off must take it away, so a live one that is not desired is drift.
|
|
230
|
+
*/
|
|
228
231
|
export declare function agoraEnvDrift(desired: Readonly<Record<string, string>>, live: Readonly<Record<string, string>>): string[];
|
|
229
232
|
/**
|
|
230
233
|
* #442: recorded once a restart has confirmed the org's live Agora process
|
package/dist/plan/agora.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chatUpstreams } from './chat-matrix.js';
|
|
2
2
|
import { FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY } from './core-files.js';
|
|
3
|
+
import { SIGN_IN_UPSTREAM_ENV, inAppHostApplied, signInUpstream, staleSignInUpstream, } from './sign-in.js';
|
|
3
4
|
/**
|
|
4
5
|
* #164: what both the planner (`diff.ts`) and the executor (`executor.ts`)
|
|
5
6
|
* need to agree on for an org's Agora -- the hostname, the values the host
|
|
@@ -288,6 +289,9 @@ export function desiredAgoraEnv(org, recorded, rawHost) {
|
|
|
288
289
|
chat: { enabled: org.chat.enabled },
|
|
289
290
|
// Agora draws call buttons only when this is true.
|
|
290
291
|
calls: { enabled: org.calls.enabled },
|
|
292
|
+
// In-app sign-in: Agora reads this at runtime (one image serves every company). Set with `SIGNIN_UPSTREAM`
|
|
293
|
+
// only, so Agora never shows the in-app screen before the login can serve it.
|
|
294
|
+
...(inAppHostApplied(org, recorded, agoraHostnameFor(org)) ? { signInInApp: true } : {}),
|
|
291
295
|
});
|
|
292
296
|
}
|
|
293
297
|
if (org.mail.mode !== 'off' && host.mailUpstream) {
|
|
@@ -309,6 +313,10 @@ export function desiredAgoraEnv(org, recorded, rawHost) {
|
|
|
309
313
|
if (org.calls.enabled && callsServiceHasName(org, recorded)) {
|
|
310
314
|
env.AGORA_CALLS_UPSTREAM = `http://${callsServiceInternalName(org)}:${CALLS_SERVICE_PORT}`;
|
|
311
315
|
}
|
|
316
|
+
// In-app sign-in: the app proxies `/sign-in/*` to the login only after its host is applied there.
|
|
317
|
+
if (inAppHostApplied(org, recorded, agoraHostnameFor(org))) {
|
|
318
|
+
env[SIGN_IN_UPSTREAM_ENV] = signInUpstream(org);
|
|
319
|
+
}
|
|
312
320
|
return { env, missing };
|
|
313
321
|
}
|
|
314
322
|
function stable(value) {
|
|
@@ -330,14 +338,20 @@ function sameEnvValue(key, desired, live) {
|
|
|
330
338
|
return false;
|
|
331
339
|
}
|
|
332
340
|
}
|
|
333
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* The desired env keys the live app lacks or holds a different value for. Extra live keys are ignored,
|
|
343
|
+
* except `SIGNIN_UPSTREAM`: the switch turned off must take it away, so a live one that is not desired is drift.
|
|
344
|
+
*/
|
|
334
345
|
export function agoraEnvDrift(desired, live) {
|
|
335
|
-
|
|
346
|
+
const drift = Object.entries(desired)
|
|
336
347
|
.filter(([key, value]) => {
|
|
337
348
|
const liveValue = live[key];
|
|
338
349
|
return liveValue === undefined || !sameEnvValue(key, value, liveValue);
|
|
339
350
|
})
|
|
340
351
|
.map(([key]) => key);
|
|
352
|
+
if (staleSignInUpstream(desired, live))
|
|
353
|
+
drift.push(SIGN_IN_UPSTREAM_ENV);
|
|
354
|
+
return drift;
|
|
341
355
|
}
|
|
342
356
|
/**
|
|
343
357
|
* #442: recorded once a restart has confirmed the org's live Agora process
|
package/dist/plan/boule.d.ts
CHANGED
|
@@ -80,7 +80,7 @@ export declare const BOULE_BRIDGE_SECRET_ENV = "ARCHON_BRIDGE_SECRET";
|
|
|
80
80
|
/** The role Boule creates from {@link BOULE_PEER_PASSWORD_ENV}, with grants on schema `authz` only. */
|
|
81
81
|
export declare const BOULE_PEER_ROLE = "authz_operator_rt";
|
|
82
82
|
/** The env names `desiredBouleEnv` can write, so the live reader knows which to read back. */
|
|
83
|
-
export declare const BOULE_MANAGED_ENV_KEYS: readonly ["APP_URL", "AUTH_ISSUER", "AUTH_ORGANIZATION_ID", "AUTH_CLIENT_ID", "BOULE_ORG_NAME", "AUTHZ_BOOTSTRAP_OPERATOR", "AUTHZ_FIRST_OWNER", "ARCHON_URL", "MAIL_FROM", "LETTERMINT_API_TOKEN", "AUTHZ_PEER_PASSWORD_OPERATOR", "ARCHON_BRIDGE_SECRET"];
|
|
83
|
+
export declare const BOULE_MANAGED_ENV_KEYS: readonly ["APP_URL", "AUTH_ISSUER", "AUTH_ORGANIZATION_ID", "AUTH_CLIENT_ID", "BOULE_ORG_NAME", "AUTHZ_BOOTSTRAP_OPERATOR", "AUTHZ_FIRST_OWNER", "ARCHON_URL", "MAIL_FROM", "LETTERMINT_API_TOKEN", "AUTHZ_PEER_PASSWORD_OPERATOR", "ARCHON_BRIDGE_SECRET", "SIGNIN_UPSTREAM"];
|
|
84
84
|
/**
|
|
85
85
|
* The env vars an org's Boule app should carry, and every input still missing
|
|
86
86
|
* to build them. Callers that write must refuse on a non-empty `missing`: a
|
|
@@ -124,5 +124,8 @@ export declare function wantedBouleBrokerEntry(org: DeclaredOrg, recorded: Reado
|
|
|
124
124
|
} | {
|
|
125
125
|
missing: BrokerEntryInput[];
|
|
126
126
|
};
|
|
127
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* The desired env keys the live app lacks or holds a different value for. Extra live keys are ignored,
|
|
129
|
+
* except `SIGNIN_UPSTREAM`: the switch turned off must take it away, so a live one that is not desired is drift.
|
|
130
|
+
*/
|
|
128
131
|
export declare function bouleEnvDrift(desired: Readonly<Record<string, string>>, live: Readonly<Record<string, string>>): string[];
|
package/dist/plan/boule.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_NO_REPLY_LOCAL_PART } from '../org.js';
|
|
2
2
|
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
3
|
+
import { SIGN_IN_UPSTREAM_ENV, inAppHostApplied, signInUpstream, staleSignInUpstream, } from './sign-in.js';
|
|
3
4
|
/**
|
|
4
5
|
* #161 (Boule part): what both the planner (`diff.ts`) and the executor
|
|
5
6
|
* (`executor.ts`) need to agree on for an org's Boule -- the hostname, the
|
|
@@ -103,6 +104,7 @@ export const BOULE_MANAGED_ENV_KEYS = [
|
|
|
103
104
|
BOULE_MAIL_TOKEN_ENV,
|
|
104
105
|
BOULE_PEER_PASSWORD_ENV,
|
|
105
106
|
BOULE_BRIDGE_SECRET_ENV,
|
|
107
|
+
SIGN_IN_UPSTREAM_ENV,
|
|
106
108
|
];
|
|
107
109
|
/**
|
|
108
110
|
* The env vars an org's Boule app should carry, and every input still missing
|
|
@@ -162,6 +164,10 @@ export function desiredBouleEnv(org, recorded, issuer, archonUrl, mailWanted = f
|
|
|
162
164
|
const mailDomain = recorded[MAIL_DOMAIN_RECORDED_KEY];
|
|
163
165
|
if (mailWanted && mailDomain)
|
|
164
166
|
env.MAIL_FROM = bouleMailFrom(org, mailDomain);
|
|
167
|
+
// In-app sign-in: the app proxies `/sign-in/*` to the login only after its host is applied there.
|
|
168
|
+
if (inAppHostApplied(org, recorded, bouleHostnameFor(org))) {
|
|
169
|
+
env[SIGN_IN_UPSTREAM_ENV] = signInUpstream(org);
|
|
170
|
+
}
|
|
165
171
|
return { env, missing };
|
|
166
172
|
}
|
|
167
173
|
/** The `AUTH_APPS` name of an org's Boule broker entry, and the scope key suffix it records under. */
|
|
@@ -201,9 +207,15 @@ export function wantedBouleBrokerEntry(org, recorded) {
|
|
|
201
207
|
},
|
|
202
208
|
};
|
|
203
209
|
}
|
|
204
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* The desired env keys the live app lacks or holds a different value for. Extra live keys are ignored,
|
|
212
|
+
* except `SIGNIN_UPSTREAM`: the switch turned off must take it away, so a live one that is not desired is drift.
|
|
213
|
+
*/
|
|
205
214
|
export function bouleEnvDrift(desired, live) {
|
|
206
|
-
|
|
215
|
+
const drift = Object.entries(desired)
|
|
207
216
|
.filter(([key, value]) => live[key] !== value)
|
|
208
217
|
.map(([key]) => key);
|
|
218
|
+
if (staleSignInUpstream(desired, live))
|
|
219
|
+
drift.push(SIGN_IN_UPSTREAM_ENV);
|
|
220
|
+
return drift;
|
|
209
221
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foundry deploys from its own pipeline, so Coolify's pull-request previews
|
|
3
|
+
* stay off on every app it manages (#788).
|
|
4
|
+
*
|
|
5
|
+
* Whether a live app has them on. `@wtfalch/providers` types the field on
|
|
6
|
+
* create and update only, and the read location (top level, or under
|
|
7
|
+
* `settings` as `is_consistent_container_name_enabled` is) is not documented,
|
|
8
|
+
* so both are read. Absent in both reads as off.
|
|
9
|
+
*/
|
|
10
|
+
export declare function previewsOn(app: unknown): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foundry deploys from its own pipeline, so Coolify's pull-request previews
|
|
3
|
+
* stay off on every app it manages (#788).
|
|
4
|
+
*
|
|
5
|
+
* Whether a live app has them on. `@wtfalch/providers` types the field on
|
|
6
|
+
* create and update only, and the read location (top level, or under
|
|
7
|
+
* `settings` as `is_consistent_container_name_enabled` is) is not documented,
|
|
8
|
+
* so both are read. Absent in both reads as off.
|
|
9
|
+
*/
|
|
10
|
+
export function previewsOn(app) {
|
|
11
|
+
const a = app;
|
|
12
|
+
return (a?.is_preview_deployments_enabled === true ||
|
|
13
|
+
a?.settings?.is_preview_deployments_enabled === true);
|
|
14
|
+
}
|
package/dist/plan/diff.js
CHANGED
|
@@ -12,7 +12,7 @@ import { planFilesKeys } from './entry-files-key.js';
|
|
|
12
12
|
import { hasWildcardGate, planGates } from './gates.js';
|
|
13
13
|
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
14
14
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
15
|
-
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
15
|
+
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
16
16
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
17
17
|
import { mailStoreSwitchChange } from './mail-store.js';
|
|
18
18
|
import { hasAdoptedResources, networkMoveNeeded, planNetworkMove } from './network-move.js';
|
|
@@ -24,6 +24,7 @@ import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
|
24
24
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
25
25
|
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryRoutes, entryRoutesMarker, } from './proxy-labels.js';
|
|
26
26
|
import { ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY, ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, signInDomainBlockedText, signInOrgDomain, } from './sign-in-org.js';
|
|
27
|
+
import { SIGN_IN_BASE_PATH, SIGN_IN_IMAGE_TAG_SUFFIX, SIGN_IN_IN_APP_SCOPE_KEY, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_LOGIN_INTERNAL_NAME, SIGN_IN_RATE_LIMIT_SCOPE_KEY, SIGN_IN_TRUSTED_SCOPE_KEY, inAppSignIn, inAppStatePairs, inAppTargets, } from './sign-in.js';
|
|
27
28
|
import { withStepSystem } from './step-system.js';
|
|
28
29
|
import { planWorkers } from './worker.js';
|
|
29
30
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
@@ -228,7 +229,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
228
229
|
// #267: after the ZITADEL org step, before any app's own issuer
|
|
229
230
|
// application. Apps created later in this run are not in the binding yet;
|
|
230
231
|
// the next run replans this step and repoints them.
|
|
231
|
-
const loginHost = planLoginHost(org, recordedIds, platformReadiness.authReady, issuer.step !== null, options.loginHostBindings ?? false);
|
|
232
|
+
const loginHost = planLoginHost(org, recordedIds, platformReadiness.authReady, issuer.step !== null, options.loginHostBindings ?? false, platformReadiness.signInLoginReady ?? false);
|
|
232
233
|
if (loginHost.step)
|
|
233
234
|
plannedSteps.push(loginHost.step);
|
|
234
235
|
// #444a: the org's one shared Postgres server and its fixed schema
|
|
@@ -1085,13 +1086,21 @@ export function isPlatformOwnOrg(org, platformOwnerOrgId) {
|
|
|
1085
1086
|
* platform's own domain, never plans the shared ZITADEL instance's own login
|
|
1086
1087
|
* host as its own -- `auth.companyincluded.com` colliding with itself.
|
|
1087
1088
|
*/
|
|
1088
|
-
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired) {
|
|
1089
|
+
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired, signInLoginReady = false) {
|
|
1089
1090
|
const none = { step: null, blocked: null };
|
|
1090
1091
|
// An own org's instance has no shared login host to bind: its issuer is the sign-in service's own.
|
|
1092
|
+
// Only in-app sign-in plans a step for it: the app hosts, through its own in-app login app.
|
|
1091
1093
|
if (ownsServices(org))
|
|
1092
|
-
return
|
|
1094
|
+
return planOwnLoginHost(org, recordedIds);
|
|
1093
1095
|
const key = resourceKey('zitadel', 'login-host', org.id);
|
|
1094
1096
|
const host = loginHostnameFor(org);
|
|
1097
|
+
// In-app sign-in: each app host gets its own binding, trusted domain and base URI. Until the second
|
|
1098
|
+
// login app that serves `/sign-in` is applied, no app host is planned (a blocked line says so).
|
|
1099
|
+
const wantedApps = inAppApps(org, recordedIds);
|
|
1100
|
+
const apps = signInLoginReady ? wantedApps : [];
|
|
1101
|
+
const inAppWaiting = wantedApps.length > 0 && !signInLoginReady
|
|
1102
|
+
? `${org.slug}'s in-app sign-in waits for the login app that serves ${SIGN_IN_BASE_PATH} (${SIGN_IN_LOGIN_INTERNAL_NAME}), which the platform has not applied`
|
|
1103
|
+
: null;
|
|
1095
1104
|
function stepFor() {
|
|
1096
1105
|
return {
|
|
1097
1106
|
step: {
|
|
@@ -1099,7 +1108,11 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1099
1108
|
system: 'zitadel',
|
|
1100
1109
|
kind: 'login-host',
|
|
1101
1110
|
key: org.id,
|
|
1102
|
-
desired:
|
|
1111
|
+
desired: apps.length > 0 ||
|
|
1112
|
+
recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ||
|
|
1113
|
+
recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY]
|
|
1114
|
+
? { host, apps }
|
|
1115
|
+
: { host },
|
|
1103
1116
|
},
|
|
1104
1117
|
step: {
|
|
1105
1118
|
description: `set ${org.slug}'s login host to ${host}`,
|
|
@@ -1107,7 +1120,7 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1107
1120
|
migrates: false,
|
|
1108
1121
|
},
|
|
1109
1122
|
},
|
|
1110
|
-
blocked:
|
|
1123
|
+
blocked: inAppWaiting,
|
|
1111
1124
|
};
|
|
1112
1125
|
}
|
|
1113
1126
|
// Already applied -- a re-run plans nothing unless the host changed, an app
|
|
@@ -1116,11 +1129,14 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1116
1129
|
// `zitadelLoginHost` always advances the recorded host to the current one,
|
|
1117
1130
|
// so a pending removal is the only way to know one is still owed, and
|
|
1118
1131
|
// nothing else here would replan the step to retry it.
|
|
1132
|
+
const targets = inAppTargets(apps, recordedIds);
|
|
1119
1133
|
if (recordedIds[key] === host &&
|
|
1120
1134
|
recordedIds[LOGIN_BINDING_SCOPE_KEY] ===
|
|
1121
|
-
loginBindingState(host, loginHostClientIds(org, recordedIds)) &&
|
|
1135
|
+
loginBindingState(host, loginHostClientIds(org, recordedIds, inAppStatePairs(targets.active).map((pair) => pair.clientId))) &&
|
|
1136
|
+
(recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1137
|
+
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
|
|
1122
1138
|
pendingRemovalHosts(recordedIds).length === 0) {
|
|
1123
|
-
return
|
|
1139
|
+
return { step: null, blocked: inAppWaiting };
|
|
1124
1140
|
}
|
|
1125
1141
|
// #177: nothing to add a trusted domain to yet -- `planIssuer` already
|
|
1126
1142
|
// reports the platform's sign-in as blocked, so this step stays silent
|
|
@@ -1132,6 +1148,55 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1132
1148
|
// repoint nothing until the port that writes the binding is wired.
|
|
1133
1149
|
return loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) ?? stepFor();
|
|
1134
1150
|
}
|
|
1151
|
+
/**
|
|
1152
|
+
* In-app sign-in for a company that runs its own services: each app host is trusted in the company's
|
|
1153
|
+
* own ZITADEL, bound in its in-app login app (`<slug>-auth-login-sign-in`) and given
|
|
1154
|
+
* `https://<app host>/sign-in/` as its Login V2 base URI. Planned once the company's own sign-in is
|
|
1155
|
+
* applied and that login app is recorded; until then one blocked line says what it waits for.
|
|
1156
|
+
* Archon's host is not here: the platform's Archon step owns it.
|
|
1157
|
+
*/
|
|
1158
|
+
function planOwnLoginHost(org, recordedIds) {
|
|
1159
|
+
const none = { step: null, blocked: null };
|
|
1160
|
+
// Nothing wanted and nothing owed.
|
|
1161
|
+
if (!inAppSignIn(org) && !recordedIds[SIGN_IN_IN_APP_SCOPE_KEY])
|
|
1162
|
+
return none;
|
|
1163
|
+
// The company's own sign-in is reported on its own line (`coolify:own-auth`).
|
|
1164
|
+
if (!ownAuthApplied(org, recordedIds))
|
|
1165
|
+
return none;
|
|
1166
|
+
const host = ownHostnames(org).auth;
|
|
1167
|
+
const wantedApps = inAppApps(org, recordedIds);
|
|
1168
|
+
const ready = Boolean(recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY]);
|
|
1169
|
+
const apps = ready ? wantedApps : [];
|
|
1170
|
+
const waiting = wantedApps.length > 0 && !ready
|
|
1171
|
+
? `${org.slug}'s in-app sign-in waits for the login app that serves ${SIGN_IN_BASE_PATH} (${org.slug}-auth-login${SIGN_IN_IMAGE_TAG_SUFFIX}), which the company's own sign-in has not applied`
|
|
1172
|
+
: null;
|
|
1173
|
+
// The company's own login host needs nothing here, so only the app hosts decide whether work is owed.
|
|
1174
|
+
const targets = inAppTargets(apps, recordedIds);
|
|
1175
|
+
if ((recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1176
|
+
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
|
|
1177
|
+
pendingRemovalHosts(recordedIds).length === 0) {
|
|
1178
|
+
return { step: null, blocked: waiting };
|
|
1179
|
+
}
|
|
1180
|
+
// Nothing to trust a domain in, or to bind in, before the org exists in the company's ZITADEL.
|
|
1181
|
+
if (!recordedIds.zitadel_org)
|
|
1182
|
+
return { step: null, blocked: waiting };
|
|
1183
|
+
return {
|
|
1184
|
+
step: {
|
|
1185
|
+
action: {
|
|
1186
|
+
system: 'zitadel',
|
|
1187
|
+
kind: 'login-host',
|
|
1188
|
+
key: org.id,
|
|
1189
|
+
desired: { host, apps },
|
|
1190
|
+
},
|
|
1191
|
+
step: {
|
|
1192
|
+
description: `set ${org.slug}'s app hosts to sign in on their own addresses`,
|
|
1193
|
+
destroys: false,
|
|
1194
|
+
migrates: false,
|
|
1195
|
+
},
|
|
1196
|
+
},
|
|
1197
|
+
blocked: waiting,
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1135
1200
|
/**
|
|
1136
1201
|
* Every sign-in app this org already has live (web or native, not api) gets
|
|
1137
1202
|
* "user info inside the ID token" switched on once: apps made before the
|
|
@@ -2905,6 +2970,21 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2905
2970
|
blocked,
|
|
2906
2971
|
};
|
|
2907
2972
|
}
|
|
2973
|
+
// #788: pull-request previews on a live app. `coolifyApp` patches them off
|
|
2974
|
+
// on every run, so replanning this one step is the whole repair.
|
|
2975
|
+
if (live.config.previewsEnabled === true) {
|
|
2976
|
+
return {
|
|
2977
|
+
step: {
|
|
2978
|
+
action,
|
|
2979
|
+
step: {
|
|
2980
|
+
description: `turn Coolify's preview deployments off for ${org.slug}'s ${label} (#788)`,
|
|
2981
|
+
destroys: false,
|
|
2982
|
+
migrates: false,
|
|
2983
|
+
},
|
|
2984
|
+
},
|
|
2985
|
+
blocked,
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2908
2988
|
// #239 (rule 4), from live state: an app with a database but no
|
|
2909
2989
|
// `DATABASE_URL_OWNER` env still has the pre-split superuser URL, or never
|
|
2910
2990
|
// got the push. Names only, never values. A reader that did not list env
|
|
@@ -3738,8 +3818,11 @@ function accessRuleDescription(org, rule) {
|
|
|
3738
3818
|
function planRateLimit(org, recordedIds) {
|
|
3739
3819
|
if (!(org.hosting.usesDomain || org.site.enabled) || !org.domain)
|
|
3740
3820
|
return null;
|
|
3741
|
-
|
|
3821
|
+
// Recorded, and the rule is the variant the switch asks for (the in-app one also covers POSTs under `/sign-in/`).
|
|
3822
|
+
if (recordedIds[resourceKey('cloudflare', 'ratelimit', org.id)] &&
|
|
3823
|
+
(recordedIds[SIGN_IN_RATE_LIMIT_SCOPE_KEY] === '1') === inAppSignIn(org)) {
|
|
3742
3824
|
return null;
|
|
3825
|
+
}
|
|
3743
3826
|
return {
|
|
3744
3827
|
action: { system: 'cloudflare', kind: 'ratelimit', key: org.id, desired: {} },
|
|
3745
3828
|
step: {
|
|
@@ -29,6 +29,11 @@ export interface RateLimitPort {
|
|
|
29
29
|
createRateLimitRule(zoneId: string, rule: RateLimitRuleInput): Promise<{
|
|
30
30
|
id: string;
|
|
31
31
|
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Deletes that one rule. Absent on a port that cannot; a step that needs it then fails by name.
|
|
34
|
+
* Rejects with a not-found error when the rule is already gone.
|
|
35
|
+
*/
|
|
36
|
+
deleteRateLimitRule?(zoneId: string, id: string): Promise<void>;
|
|
32
37
|
}
|
|
33
38
|
/** Edge answers a create another create of the same rule is making `conflict` + `busy`. */
|
|
34
39
|
export declare const EDGE_RATELIMITS_BUSY_ATTEMPTS = 5;
|
|
@@ -45,14 +45,16 @@ export function createEdgeRateLimits(client, options = {}) {
|
|
|
45
45
|
return;
|
|
46
46
|
const { rules } = await retrying(() => client.list({ zoneId, description }));
|
|
47
47
|
const view = rules.find((r) => r.description === description && r.state !== 'removed');
|
|
48
|
-
if (view && view.owner === null)
|
|
49
|
-
|
|
50
|
-
if (result.skipped.length > 0) {
|
|
51
|
-
throw new Error(`edge rate limits: rule '${description}' in zone ${zoneId} cannot be adopted (${result.skipped[0]?.reason}); remove or fix it by hand`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
48
|
+
if (view && view.owner === null)
|
|
49
|
+
await adoptView(zoneId, view);
|
|
54
50
|
adopted.add(key);
|
|
55
51
|
};
|
|
52
|
+
const adoptView = async (zoneId, view) => {
|
|
53
|
+
const result = await writeSlot(() => retrying(() => client.adopt({ rules: [{ zoneId, description: view.description ?? '', id: view.id }] })));
|
|
54
|
+
if (result.skipped.length > 0) {
|
|
55
|
+
throw new Error(`edge rate limits: rule '${view.description}' in zone ${zoneId} cannot be adopted (${result.skipped[0]?.reason}); remove or fix it by hand`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
56
58
|
return {
|
|
57
59
|
async listRateLimitRules(zoneId) {
|
|
58
60
|
const { rules } = await retrying(() => client.list({ zoneId }));
|
|
@@ -60,6 +62,14 @@ export function createEdgeRateLimits(client, options = {}) {
|
|
|
60
62
|
.filter((r) => r.state !== 'removed')
|
|
61
63
|
.map((r) => ({ id: r.id, description: r.description }));
|
|
62
64
|
},
|
|
65
|
+
async deleteRateLimitRule(zoneId, id) {
|
|
66
|
+
// Edge removes only a rule it owns: a live rule it has no row for is adopted first, as a create does.
|
|
67
|
+
const { rules } = await retrying(() => client.list({ zoneId }));
|
|
68
|
+
const view = rules.find((r) => r.id === id && r.state !== 'removed');
|
|
69
|
+
if (view && view.owner === null)
|
|
70
|
+
await adoptView(zoneId, view);
|
|
71
|
+
await writeSlot(() => retrying(() => client.remove(zoneId, id)));
|
|
72
|
+
},
|
|
63
73
|
async createRateLimitRule(zoneId, rule) {
|
|
64
74
|
await adoptLive(zoneId, rule.description);
|
|
65
75
|
const create = () => writeSlot(() => retrying(() => client.create({ zoneId, ...rule })));
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -107,7 +107,10 @@ export interface CoolifyPort {
|
|
|
107
107
|
custom_labels?: string | null;
|
|
108
108
|
settings?: {
|
|
109
109
|
is_consistent_container_name_enabled?: boolean;
|
|
110
|
+
is_preview_deployments_enabled?: boolean;
|
|
110
111
|
};
|
|
112
|
+
/** #788: where Coolify reports previews is not documented, so `previewsOn` reads both places. */
|
|
113
|
+
is_preview_deployments_enabled?: boolean;
|
|
111
114
|
custom_network_aliases?: string | null;
|
|
112
115
|
health_check_enabled?: boolean;
|
|
113
116
|
ports_exposes?: string;
|
|
@@ -120,6 +123,8 @@ export interface CoolifyPort {
|
|
|
120
123
|
custom_docker_run_options?: string | null;
|
|
121
124
|
}>;
|
|
122
125
|
createGithubAppApplication(input: {
|
|
126
|
+
/** #788: previews off; every git app create sends `false`. */
|
|
127
|
+
is_preview_deployments_enabled?: boolean;
|
|
123
128
|
project_uuid: string;
|
|
124
129
|
server_uuid: string;
|
|
125
130
|
/** The Docker network the app joins (`plan/destinations.ts`); absent: the server's default. */
|
|
@@ -1140,6 +1145,14 @@ export interface ZitadelPort {
|
|
|
1140
1145
|
* the binding holds; `zitadelApp` never sets it at creation.
|
|
1141
1146
|
*/
|
|
1142
1147
|
updateOidcAppLoginBaseUri(orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
|
|
1148
|
+
/**
|
|
1149
|
+
* Lets each app's own Login V2 base URI count: foundry starts ZITADEL with `LOGINV2_REQUIRED=true`,
|
|
1150
|
+
* which makes the instance ignore it. The host sets the instance feature (`PUT /v2/features/instance`
|
|
1151
|
+
* with `loginV2.required` false and the instance default as `baseUri`). Called before an app is
|
|
1152
|
+
* pointed at its own `/sign-in/` on a company that runs its own ZITADEL; the shared instance's
|
|
1153
|
+
* bindings port does the same for the platform's. Optional: absent, nothing is called.
|
|
1154
|
+
*/
|
|
1155
|
+
allowPerAppLoginBaseUri?(instanceBaseUri: string): Promise<void>;
|
|
1143
1156
|
/**
|
|
1144
1157
|
* Occupies `userName` in `orgId` with a machine user that has no role and
|
|
1145
1158
|
* no token, so nobody can register under that name. Idempotent: a user
|
|
@@ -2006,6 +2019,13 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2006
2019
|
* client id that binding holds. Apps created later are not in it yet:
|
|
2007
2020
|
* the next run replans (`diff.ts`'s `planLoginHost`) and moves them.
|
|
2008
2021
|
*
|
|
2022
|
+
* In-app sign-in (`signIn.inApp`, `plan/sign-in.ts` has the order): an app host is trusted first. It is
|
|
2023
|
+
* bound, and its app pointed at `/sign-in/`, only once its app runs `SIGNIN_UPSTREAM` (the app step
|
|
2024
|
+
* records that). The bind moves the app's client id off the company's binding in the same write (the
|
|
2025
|
+
* login refuses a client id in two bindings); a company binding left with no client is removed.
|
|
2026
|
+
* Switched off, the client id goes back and the base URI returns to the login host, and a trusted
|
|
2027
|
+
* domain is given up only after the app dropped the env.
|
|
2028
|
+
*
|
|
2009
2029
|
* #274: when the org's host has moved (`recorded[key]`, the *previous*
|
|
2010
2030
|
* run's resourceId, differs from this run's `host`), also removes every
|
|
2011
2031
|
* host that is no longer the org's current one from both places --
|
|
@@ -2067,6 +2087,12 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2067
2087
|
* the snapshot taken before the repoint still named it.
|
|
2068
2088
|
*/
|
|
2069
2089
|
private zitadelLoginHost;
|
|
2090
|
+
/**
|
|
2091
|
+
* The host table of a company's own in-app login app as a bindings port that collects its edits and
|
|
2092
|
+
* writes them once on `flush` (one redeploy, none when nothing changed). Used for a company that runs
|
|
2093
|
+
* its own services, which has no host-supplied bindings port.
|
|
2094
|
+
*/
|
|
2095
|
+
private ownSignInBindings;
|
|
2070
2096
|
/**
|
|
2071
2097
|
* `plan/diff.ts`'s `planCustomers` (plan gap A9): the org's customers sign
|
|
2072
2098
|
* in through a second ZITADEL org, `<slug> customers`, with
|
|
@@ -2825,6 +2851,18 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2825
2851
|
* behind it.
|
|
2826
2852
|
*/
|
|
2827
2853
|
private coolifyAgora;
|
|
2854
|
+
/**
|
|
2855
|
+
* Records whether an app's running env holds `SIGNIN_UPSTREAM` (the app's host while it does, `''` once it
|
|
2856
|
+
* does not), so the login-host step knows when to point the base URI at the app and when it may give the
|
|
2857
|
+
* host up. Written only when the value changes.
|
|
2858
|
+
*/
|
|
2859
|
+
private recordSignInEnvLive;
|
|
2860
|
+
/**
|
|
2861
|
+
* In-app sign-in switched off (or its host given up): the app's live `SIGNIN_UPSTREAM` is deleted, because
|
|
2862
|
+
* `bulkUpsertEnvs` never removes a key. Left in place, the app would keep proxying `/sign-in/*` to the login.
|
|
2863
|
+
* Nothing happens while the env is desired or the app does not hold it.
|
|
2864
|
+
*/
|
|
2865
|
+
private dropStaleSignInUpstream;
|
|
2828
2866
|
/**
|
|
2829
2867
|
* `plan/diff.ts`'s `planBoule`: provisions the org's own Boule
|
|
2830
2868
|
* (https://github.com/wtfalch/boule) as one more Coolify application, from
|
|
@@ -3433,3 +3471,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
3433
3471
|
* `:`. It never scans the path or the response body.
|
|
3434
3472
|
*/
|
|
3435
3473
|
export declare function isNotFoundError(error: unknown): boolean;
|
|
3474
|
+
/** True when ZITADEL refused an update because the value already is the wanted one. Matches the error code, never the free text. */
|
|
3475
|
+
export declare function isNoChangesError(error: unknown): boolean;
|
|
3476
|
+
/**
|
|
3477
|
+
* {@link ZitadelPort.updateOidcAppLoginBaseUri} that counts an app already holding the wanted base URI as
|
|
3478
|
+
* done: ZITADEL answers that write with "No changes" and a repeat apply must not fail on it.
|
|
3479
|
+
*/
|
|
3480
|
+
export declare function setOidcAppLoginBaseUri(zitadel: Pick<ZitadelPort, 'updateOidcAppLoginBaseUri'>, orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
|