@wtfalch/foundry 0.26.4 → 0.28.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/calls-sfu.d.ts +19 -0
- package/dist/calls-sfu.js +31 -8
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/migrations/0050_platform_sfu_connection_revision.sql +7 -0
- package/dist/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +44 -1
- package/dist/plan/executor.d.ts +35 -7
- package/dist/plan/executor.js +52 -8
- package/dist/plan/index.d.ts +3 -3
- package/dist/plan/index.js +2 -2
- package/dist/plan/live.d.ts +3 -3
- package/dist/plan/plan-apply.d.ts +8 -0
- package/dist/plan/plan-apply.js +8 -0
- package/dist/plan/platform-diff.d.ts +32 -0
- package/dist/plan/platform-diff.js +77 -14
- package/dist/plan/platform-org.d.ts +3 -4
- package/dist/plan/platform-providers.d.ts +19 -2
- package/dist/plan/platform-providers.js +108 -15
- package/dist/plan/platform-sfu.d.ts +10 -0
- package/dist/plan/platform-sfu.js +39 -0
- package/dist/plan/providers-readers.d.ts +5 -2
- package/dist/platform.d.ts +11 -1
- package/dist/platform.js +6 -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/calls-sfu.d.ts
CHANGED
|
@@ -154,6 +154,19 @@ export declare const CALLS_SFU_PRIVATE_URL = "http://10.10.10.105:7880";
|
|
|
154
154
|
* hostname out from under it.
|
|
155
155
|
*/
|
|
156
156
|
export declare const CALLS_SFU_PUBLIC_URL = "https://calls-sfu.oikos.host";
|
|
157
|
+
/**
|
|
158
|
+
* A host's own SFU addresses, for a stack where the shared SFU is not the
|
|
159
|
+
* estate's (a local run). Optional on `FoundryStepExecutorPorts`; absent, the
|
|
160
|
+
* two constants above apply unchanged.
|
|
161
|
+
*/
|
|
162
|
+
export interface CallsSfuUrls {
|
|
163
|
+
/** What a browser reaches: becomes the calls service's `LIVEKIT_PUBLIC_URL`. */
|
|
164
|
+
publicUrl: string;
|
|
165
|
+
/** What the calls service reaches: becomes its `LIVEKIT_URL`. */
|
|
166
|
+
privateUrl: string;
|
|
167
|
+
}
|
|
168
|
+
/** The SFU addresses to use: the override, validated, else the constants. Throws naming the bad field. */
|
|
169
|
+
export declare function resolveCallsSfuUrls(override?: CallsSfuUrls): CallsSfuUrls;
|
|
157
170
|
/**
|
|
158
171
|
* Exported for the test file. Reads `ufw status`'s own report rather than
|
|
159
172
|
* assuming the exit code of a blind `ufw delete allow 7880/tcp` -- real
|
|
@@ -190,6 +203,12 @@ export declare function cloudflaredEnvFile(token: string): string;
|
|
|
190
203
|
* writes the set it was given.
|
|
191
204
|
*/
|
|
192
205
|
export declare function livekitConfigYaml(config: CallsSfuConfig): string;
|
|
206
|
+
/** The command that brings both compose services up; part of what {@link callsSfuFilesVersion} hashes. */
|
|
207
|
+
export declare const CALLS_SFU_UP_COMMAND = "docker compose -f /opt/livekit/docker-compose.yml up -d";
|
|
208
|
+
/** Run first, so the SSH session this runs over is never refused. */
|
|
209
|
+
export declare const CALLS_SFU_SSH_RULE_COMMAND = "ufw allow 22/tcp";
|
|
210
|
+
/** Run after any unscoped signalling rule is removed, in this order. */
|
|
211
|
+
export declare const CALLS_SFU_FIREWALL_COMMANDS: readonly string[];
|
|
193
212
|
/**
|
|
194
213
|
* Writes all three files, brings LiveKit and `cloudflared` up, then narrows
|
|
195
214
|
* the VM firewall to exactly what #122's and #127's "Decided" sections name
|
package/dist/calls-sfu.js
CHANGED
|
@@ -144,6 +144,20 @@ export const CALLS_SFU_PRIVATE_URL = `http://10.10.10.105:${LIVEKIT_SIGNALLING_P
|
|
|
144
144
|
* hostname out from under it.
|
|
145
145
|
*/
|
|
146
146
|
export const CALLS_SFU_PUBLIC_URL = 'https://calls-sfu.oikos.host';
|
|
147
|
+
/** The SFU addresses to use: the override, validated, else the constants. Throws naming the bad field. */
|
|
148
|
+
export function resolveCallsSfuUrls(override) {
|
|
149
|
+
if (!override)
|
|
150
|
+
return { publicUrl: CALLS_SFU_PUBLIC_URL, privateUrl: CALLS_SFU_PRIVATE_URL };
|
|
151
|
+
for (const field of ['publicUrl', 'privateUrl']) {
|
|
152
|
+
const value = override[field];
|
|
153
|
+
if (typeof value !== 'string' ||
|
|
154
|
+
!URL.canParse(value) ||
|
|
155
|
+
!/^https?:$/.test(new URL(value).protocol)) {
|
|
156
|
+
throw new Error(`callsSfuUrls.${field} must be an absolute http(s) URL`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { publicUrl: override.publicUrl, privateUrl: override.privateUrl };
|
|
160
|
+
}
|
|
147
161
|
/**
|
|
148
162
|
* Matches `ufw status`'s report of the exact wide-open rule PR 125 added
|
|
149
163
|
* (`ufw allow 7880/tcp`) -- "Anywhere" in the From column -- and not the
|
|
@@ -225,6 +239,19 @@ rtc:
|
|
|
225
239
|
node_ip: ${address}
|
|
226
240
|
${keysYaml}`;
|
|
227
241
|
}
|
|
242
|
+
/** The command that brings both compose services up; part of what {@link callsSfuFilesVersion} hashes. */
|
|
243
|
+
export const CALLS_SFU_UP_COMMAND = `docker compose -f ${LIVEKIT_COMPOSE_PATH} up -d`;
|
|
244
|
+
/** Run first, so the SSH session this runs over is never refused. */
|
|
245
|
+
export const CALLS_SFU_SSH_RULE_COMMAND = 'ufw allow 22/tcp';
|
|
246
|
+
/** Run after any unscoped signalling rule is removed, in this order. */
|
|
247
|
+
export const CALLS_SFU_FIREWALL_COMMANDS = [
|
|
248
|
+
`ufw allow from ${CALLS_SFU_PRIVATE_CIDR} to any port ${LIVEKIT_SIGNALLING_PORT} proto tcp`,
|
|
249
|
+
`ufw allow ${LIVEKIT_MEDIA_TCP_PORT}/tcp`,
|
|
250
|
+
`ufw allow ${LIVEKIT_MEDIA_UDP_RANGE}/udp`,
|
|
251
|
+
'ufw default deny incoming',
|
|
252
|
+
'ufw default allow outgoing',
|
|
253
|
+
'ufw --force enable',
|
|
254
|
+
];
|
|
228
255
|
async function run(host, command) {
|
|
229
256
|
const result = await host.exec(command);
|
|
230
257
|
if (result.exitCode !== 0) {
|
|
@@ -276,16 +303,12 @@ export async function applyCallsSfu(host, config) {
|
|
|
276
303
|
await host.writeFile(LIVEKIT_COMPOSE_PATH, dockerComposeYaml());
|
|
277
304
|
await writeFileAtomically(host, LIVEKIT_CONFIG_PATH, livekitConfigYaml(config));
|
|
278
305
|
await host.writeFile(CLOUDFLARED_ENV_PATH, cloudflaredEnvFile(config.cloudflaredTunnelToken));
|
|
279
|
-
await run(host,
|
|
280
|
-
await run(host,
|
|
306
|
+
await run(host, CALLS_SFU_UP_COMMAND);
|
|
307
|
+
await run(host, CALLS_SFU_SSH_RULE_COMMAND);
|
|
281
308
|
const status = await host.exec('ufw status');
|
|
282
309
|
if (status.exitCode === 0 && hasUnscopedSignallingRule(status.stdout)) {
|
|
283
310
|
await run(host, `ufw delete allow ${LIVEKIT_SIGNALLING_PORT}/tcp`);
|
|
284
311
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
await run(host, `ufw allow ${LIVEKIT_MEDIA_UDP_RANGE}/udp`);
|
|
288
|
-
await run(host, 'ufw default deny incoming');
|
|
289
|
-
await run(host, 'ufw default allow outgoing');
|
|
290
|
-
await run(host, 'ufw --force enable');
|
|
312
|
+
for (const command of CALLS_SFU_FIREWALL_COMMANDS)
|
|
313
|
+
await run(host, command);
|
|
291
314
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, } from './org.js';
|
|
2
2
|
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
|
|
3
|
-
export type { DeclaredPlatform, PlatformService, PlatformServiceKey } from './platform.js';
|
|
3
|
+
export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, } from './platform.js';
|
|
4
4
|
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
5
5
|
import { type PinnedVersions } from './bootstrap/versions.js';
|
|
6
6
|
export type { PinnedVersions };
|
|
@@ -22,7 +22,7 @@ export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, File
|
|
|
22
22
|
export { AdoptRefusedError } from './plan/index.js';
|
|
23
23
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
24
24
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, DnsPort, EdgePlatformAccess, EdgePlatformDns, EdgePlatformRateLimits, EdgePlatformWorkers, EdgePlatformZones, EdgeZonesOptions, ZonePort, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, EdgeWorkersOptions, EdgePlatformTunnels, TunnelPort, } from './plan/index.js';
|
|
25
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
25
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
26
26
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
27
27
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
28
28
|
export type { PlatformOrgClientFactories } from './plan/index.js';
|
|
@@ -41,13 +41,14 @@ export { checkMailIdentityAudience } from './plan/index.js';
|
|
|
41
41
|
export type { MailAllowlistApp } from './plan/index.js';
|
|
42
42
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
43
43
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
44
|
-
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
|
|
44
|
+
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
|
|
45
45
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
|
|
46
46
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
|
|
47
47
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
|
48
48
|
export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './plan/index.js';
|
|
49
49
|
export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './plan/index.js';
|
|
50
|
-
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './plan/index.js';
|
|
50
|
+
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './plan/index.js';
|
|
51
|
+
export type { CallsSfuUrls } from './calls-sfu.js';
|
|
51
52
|
export type { CallsSfuSshSession } from './calls-sfu-ssh-adapter.js';
|
|
52
53
|
export { connectCallsSfuSsh } from './calls-sfu-ssh-client.js';
|
|
53
54
|
export type { CallsSfuSshConnectOptions } from './calls-sfu-ssh-client.js';
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { createPostgresAuditLog } from './store/audit.js';
|
|
|
13
13
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
14
14
|
export { AdoptRefusedError } from './plan/index.js';
|
|
15
15
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
16
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
16
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
17
17
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
18
18
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
19
19
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
@@ -26,7 +26,7 @@ export { checkMailAppsAllowlist } from './plan/index.js';
|
|
|
26
26
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
27
27
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
|
|
28
28
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
|
29
|
-
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './plan/index.js';
|
|
29
|
+
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './plan/index.js';
|
|
30
30
|
export { connectCallsSfuSsh } from './calls-sfu-ssh-client.js';
|
|
31
31
|
export { UnknownCallsSfuHostKeyError, toCallsSfuHostPort, applyCallsSfuOverSession, } from './calls-sfu-ssh-adapter.js';
|
|
32
32
|
export { mintLivekitCredentials } from './calls-sfu-keys.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the platform's `sfu.connectionRevision`, a value a host
|
|
2
|
+
-- changes when the SFU VM's SSH key, host key or tunnel token changes, so the
|
|
3
|
+
-- platform plan shows the `sfu` step again. Declared; null until set.
|
|
4
|
+
-- Mirrors src/store/schema.ts's `foundryDeclaredPlatform` table addition.
|
|
5
|
+
|
|
6
|
+
alter table foundry_declared_platform
|
|
7
|
+
add column if not exists sfu_connection_revision text;
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface DraftStep {
|
|
|
43
43
|
export interface PlanOrgOptions {
|
|
44
44
|
/** #267: the executor was built with a `loginHostBindings` port. */
|
|
45
45
|
loginHostBindings?: boolean;
|
|
46
|
+
/** The executor's ZITADEL client has `updateOidcAppIdTokenUserinfoAssertion`; without it `planIdTokenUserinfo` plans nothing. */
|
|
47
|
+
zitadelIdTokenUserinfo?: boolean;
|
|
46
48
|
/** F5: the executor was built with a `broker` port. */
|
|
47
49
|
broker?: boolean;
|
|
48
50
|
/** #161: the slugs of the other declared orgs, so a Boule whose hostname another org's slug would take is blocked. */
|
package/dist/plan/diff.js
CHANGED
|
@@ -12,7 +12,7 @@ import { appHostname, entryHostnames, tunnelDnsHostnames, tunnelDomainList } fro
|
|
|
12
12
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
13
13
|
import { LOGIN_BINDING_SCOPE_KEY, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
14
14
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
15
|
-
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, } from './platform-diff.js';
|
|
15
|
+
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
16
16
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
17
17
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
18
18
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec } from './proxy-labels.js';
|
|
@@ -441,6 +441,10 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
441
441
|
plannedSteps.push(step);
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
|
+
// Last: a repair that fails must not stop any step after it (`runSteps` stops at the first failure).
|
|
445
|
+
const idTokenUserinfo = planIdTokenUserinfo(org, live, recordedIds, plannedSteps, platformReadiness.authReady, options.zitadelIdTokenUserinfo ?? false);
|
|
446
|
+
if (idTokenUserinfo)
|
|
447
|
+
plannedSteps.push(idTokenUserinfo);
|
|
444
448
|
// #10 (resolved): "foundry manages only what the declared org names";
|
|
445
449
|
// anything left unclaimed is listed, never proposed for deletion.
|
|
446
450
|
// A resource foundry made for the org is not a finding: only a resource
|
|
@@ -861,6 +865,45 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
861
865
|
// repoint nothing until the port that writes the binding is wired.
|
|
862
866
|
return loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) ?? stepFor(false);
|
|
863
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Every sign-in app this org already has live (web or native, not api) gets
|
|
870
|
+
* "user info inside the ID token" switched on once: apps made before the
|
|
871
|
+
* setting existed have none, and `@wtfalch/auth` builds the signed-in user
|
|
872
|
+
* from ID token claims only. One `zitadel:id-token-userinfo` step names the
|
|
873
|
+
* apps whose recorded id differs from their marker
|
|
874
|
+
* ({@link zitadelAppIdTokenUserinfoAppliedScopeKey}); a re-created app has a
|
|
875
|
+
* new id, so it is named again. Plans nothing when the host's ZITADEL client
|
|
876
|
+
* lacks the method, before the org and project are recorded, or while
|
|
877
|
+
* the platform's sign-in is not ready.
|
|
878
|
+
*/
|
|
879
|
+
function planIdTokenUserinfo(org, live, recordedIds, plannedSteps, authReady, wired) {
|
|
880
|
+
if (!wired || !authReady || !recordedIds.zitadel_org || !recordedIds.zitadel_project)
|
|
881
|
+
return null;
|
|
882
|
+
const apps = live
|
|
883
|
+
.filter((r) => r.system === 'zitadel' &&
|
|
884
|
+
r.kind === 'app' &&
|
|
885
|
+
(r.config.appType === 'web' || r.config.appType === 'native'))
|
|
886
|
+
.map((r) => r.key)
|
|
887
|
+
// An app this plan recreates gets its marker when it is made.
|
|
888
|
+
.filter((appKey) => !plannedSteps.some((s) => s.action.system === 'zitadel' &&
|
|
889
|
+
s.action.kind === 'app' &&
|
|
890
|
+
s.action.key === appKey &&
|
|
891
|
+
s.action.desired.recreate === true))
|
|
892
|
+
.filter((appKey) => {
|
|
893
|
+
const appId = recordedIds[`zitadel:app:${appKey}`];
|
|
894
|
+
return (Boolean(appId) && recordedIds[zitadelAppIdTokenUserinfoAppliedScopeKey(appKey)] !== appId);
|
|
895
|
+
});
|
|
896
|
+
if (apps.length === 0)
|
|
897
|
+
return null;
|
|
898
|
+
return {
|
|
899
|
+
action: { system: 'zitadel', kind: 'id-token-userinfo', key: org.id, desired: { apps } },
|
|
900
|
+
step: {
|
|
901
|
+
description: `switch on user info in the ID token for ${org.slug}'s sign-in apps (${apps.join(', ')})`,
|
|
902
|
+
destroys: false,
|
|
903
|
+
migrates: false,
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
}
|
|
864
907
|
/** Why a login-host step cannot run yet, or `null` when nothing is missing. */
|
|
865
908
|
function loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) {
|
|
866
909
|
if (!bindingsWired) {
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OrgMailOptions, OrgMailOrg, OrgMailResult } from '@wtfalch/email/postmaster';
|
|
2
|
+
import { type CallsSfuUrls } from '../calls-sfu.js';
|
|
2
3
|
import type { DeclaredOrg, HostingBuildPack } from '../org.js';
|
|
3
4
|
import type { DeclaredOrgStore } from '../store.js';
|
|
4
5
|
import { type BrokerPort } from './broker-entries.js';
|
|
@@ -518,7 +519,7 @@ export interface WorkersPort {
|
|
|
518
519
|
/**
|
|
519
520
|
* One DNS record a Lettermint domain's own response says must be published --
|
|
520
521
|
* `purpose` is Lettermint's own label (`return_path`, `dmarc`, `dkim_legacy`,
|
|
521
|
-
* `dkim_primary`, `dkim_secondary`), replacing
|
|
522
|
+
* `dkim_primary`, `dkim_secondary`), replacing the earlier provider's `record` field, from
|
|
522
523
|
* {@link LettermintPort.createDomain}/{@link LettermintPort.getDomain}, the
|
|
523
524
|
* same shape https://lettermint.co/docs/api-reference/team/0.0.1/lettermint-team-openapi.json's
|
|
524
525
|
* `DomainDnsRecordData` documents (`fqdn`/`content` renamed to this port's own
|
|
@@ -551,17 +552,17 @@ export interface LettermintProject {
|
|
|
551
552
|
* has no Lettermint client yet, unlike Coolify/Cloudflare/ZITADEL above, so
|
|
552
553
|
* this port names Lettermint's own REST shapes directly
|
|
553
554
|
* (`.plans/2026-09-28-lettermint-api.md`, its own primary-source citations).
|
|
554
|
-
* Replaces
|
|
555
|
+
* Replaces the earlier mail port (#138/#140), trimmed to what `coolifyApp`/`coolifyMail`
|
|
555
556
|
* below actually call.
|
|
556
557
|
*
|
|
557
|
-
* Unlike
|
|
558
|
+
* Unlike the earlier provider's per-domain sending key, a Lettermint domain is
|
|
558
559
|
* unrestricted by default -- any project in the team could send from it --
|
|
559
560
|
* so a project's own domain access is a separate call
|
|
560
561
|
* ({@link restrictDomainToProjects}), and the credential unit is a *project*,
|
|
561
562
|
* not a per-domain key: Lettermint's API mints, at most, one live token per
|
|
562
563
|
* project and has no call to list or revoke an individual token within one
|
|
563
564
|
* (`.plans/2026-09-28-lettermint-api.md` §4), so `<slug>-app` is a project
|
|
564
|
-
* name, never suffixed with a domain id the way
|
|
565
|
+
* name, never suffixed with a domain id the way the earlier provider's key name was.
|
|
565
566
|
*/
|
|
566
567
|
export interface LettermintPort {
|
|
567
568
|
/**
|
|
@@ -584,7 +585,7 @@ export interface LettermintPort {
|
|
|
584
585
|
/**
|
|
585
586
|
* Asks Lettermint to re-check DNS now. Never polled or awaited to
|
|
586
587
|
* completion -- the same "do not block the plan on DNS propagation" rule
|
|
587
|
-
*
|
|
588
|
+
* the earlier mail port followed: record the status, and let the next
|
|
588
589
|
* apply re-check it.
|
|
589
590
|
*/
|
|
590
591
|
verifyDomain(id: string): Promise<{
|
|
@@ -1009,6 +1010,18 @@ export interface ZitadelPort {
|
|
|
1009
1010
|
* the binding holds; `zitadelApp` never sets it at creation.
|
|
1010
1011
|
*/
|
|
1011
1012
|
updateOidcAppLoginBaseUri(orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
|
|
1013
|
+
/**
|
|
1014
|
+
* Switches on "user info inside the ID token" for an already-created app,
|
|
1015
|
+
* read-modify-write (`@wtfalch/providers`' `ZitadelClient.
|
|
1016
|
+
* updateOidcAppIdTokenUserinfoAssertion`): ZITADEL defaults it to off, and
|
|
1017
|
+
* `@wtfalch/auth` builds the signed-in user from ID token claims only, so
|
|
1018
|
+
* an app made without it has no `email` claim. Resolves `true` when it
|
|
1019
|
+
* wrote the setting, `false` when the app already had it on (no write).
|
|
1020
|
+
* Optional: a host whose client lacks it gets no repair step planned
|
|
1021
|
+
* (`FoundryPlanApplyOptions.zitadelIdTokenUserinfo`), and nothing throws.
|
|
1022
|
+
* `createOidcApp` and `createOidcAppWithSecret` send it on themselves.
|
|
1023
|
+
*/
|
|
1024
|
+
updateOidcAppIdTokenUserinfoAssertion?(orgId: string, projectId: string, appId: string): Promise<boolean>;
|
|
1012
1025
|
}
|
|
1013
1026
|
/**
|
|
1014
1027
|
* #274 fix round 4: bridges `@wtfalch/providers`' real `ZitadelClient.
|
|
@@ -1377,6 +1390,12 @@ export interface FoundryStepExecutorPorts {
|
|
|
1377
1390
|
jobsScheduler?: JobsSchedulerPort;
|
|
1378
1391
|
/** The shared calls SFU's own LiveKit credentials (#145) -- see {@link CallsSfuPort}'s own doc comment. */
|
|
1379
1392
|
callsSfu: CallsSfuPort;
|
|
1393
|
+
/**
|
|
1394
|
+
* The calls service's `LIVEKIT_URL` / `LIVEKIT_PUBLIC_URL`, for a host whose
|
|
1395
|
+
* SFU is not the estate's shared one. Absent, the estate's addresses apply.
|
|
1396
|
+
* Refused (the calls-service step throws) unless both are absolute http(s) URLs.
|
|
1397
|
+
*/
|
|
1398
|
+
callsSfuUrls?: CallsSfuUrls;
|
|
1380
1399
|
/**
|
|
1381
1400
|
* #113: the org's old-mail copy into Stalwart, ahead of a `received` ->
|
|
1382
1401
|
* `hosted` MX cutover -- see {@link MailCopyPort}'s own doc comment.
|
|
@@ -1696,6 +1715,15 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1696
1715
|
* recorded).
|
|
1697
1716
|
*/
|
|
1698
1717
|
private zitadelOwner;
|
|
1718
|
+
/**
|
|
1719
|
+
* Switches on "user info inside the ID token" for each live sign-in app
|
|
1720
|
+
* the planner named. An app's marker (its ZITADEL app id) is recorded and
|
|
1721
|
+
* persisted only after its call resolved to a boolean, so a call that throws
|
|
1722
|
+
* or resolves to anything else leaves no marker and the next plan names the
|
|
1723
|
+
* app again. The error names ids only,
|
|
1724
|
+
* never the cause.
|
|
1725
|
+
*/
|
|
1726
|
+
private zitadelIdTokenUserinfo;
|
|
1699
1727
|
/**
|
|
1700
1728
|
* `plan/diff.ts`'s `planLoginHost` (#267): adds the org's own company
|
|
1701
1729
|
* login host (`action.desired.host`) to the ZITADEL instance's trusted
|
|
@@ -1931,7 +1959,7 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1931
1959
|
*
|
|
1932
1960
|
* Mint rule ("Credential model"): the project is healthy when the app env
|
|
1933
1961
|
* holds `LETTERMINT_API_TOKEN` and Lettermint lists exactly one project
|
|
1934
|
-
* named `<slug>-app` -- unlike
|
|
1962
|
+
* named `<slug>-app` -- unlike the earlier provider's key, a Lettermint project's name
|
|
1935
1963
|
* carries no domain id, since a domain switch is handled entirely by
|
|
1936
1964
|
* {@link LettermintPort.restrictDomainToProjects} below and never needs a
|
|
1937
1965
|
* fresh project. Otherwise a fresh project is created and its token
|
|
@@ -2726,7 +2754,7 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2726
2754
|
* upsert `publishMailDnsRecords` above uses for Stalwart's own records --
|
|
2727
2755
|
* except an existing `_dmarc` record is never overwritten (a stricter
|
|
2728
2756
|
* policy an org already set, `p=quarantine`/`p=reject`, should not be
|
|
2729
|
-
* relaxed back to Lettermint's own default).
|
|
2757
|
+
* relaxed back to Lettermint's own default). The earlier provider's domain also
|
|
2730
2758
|
* carried an apex `SPF` TXT record that had to be merged into whatever SPF
|
|
2731
2759
|
* record already existed there (RFC 7208: two `v=spf1` TXT records on one
|
|
2732
2760
|
* name is a permerror); Lettermint's own DNS records
|
package/dist/plan/executor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { URL } from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveCallsSfuUrls } from '../calls-sfu.js';
|
|
4
4
|
import { APP_ENTRY_KEY, appEntry, filesCollectionEnvName, ownerMailboxRecorded, ownerMailboxes, sharedSecretRef, } from '../org.js';
|
|
5
5
|
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_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatIntrospectionAppName, desiredAgoraEnv, entryClientIdScopeKey, } from './agora.js';
|
|
6
6
|
import { BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_DATABASE_SCOPE_KEY, BOULE_DEPLOYED_SCOPE_KEY, bouleEnvDrift, bouleHostnameFor, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
@@ -24,7 +24,7 @@ import { healthCheckSettings } from './health-check-settings.js';
|
|
|
24
24
|
import { appHostname, coolifyTunnelDomains, entryHostnames, tunnelDnsHostnames, tunnelDomainList, tunnelIngressHostnames, } from './hostnames.js';
|
|
25
25
|
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, customersEntryKeys, loginBindingState, loginHostClientIds, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
26
26
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
27
|
-
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appFolderDiffers, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, } from './platform-diff.js';
|
|
27
|
+
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appFolderDiffers, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
28
28
|
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, } from './platform-providers.js';
|
|
29
29
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryProxyLabels, sameLabelSets, } from './proxy-labels.js';
|
|
30
30
|
import { entryWorkerAppliedScopeKey, entryWorkerExclusionsScopeKey, entryWorkerRetiredScopeKey, entryWorkerRouteIdScopeKey, entryWorkerRoutePatternScopeKey, entryWorkerRoutePendingScopeKey, entryWorkerRouteZoneScopeKey, entryWorkerUploadedScopeKey, } from './worker.js';
|
|
@@ -354,10 +354,10 @@ const CMS_DATABASE_SCOPE_KEY = 'coolify_cms_database';
|
|
|
354
354
|
* domain choice, the same way {@link APP_CLIENT_ID_SCOPE_KEY} carries a
|
|
355
355
|
* public value out. All of that org's Lettermint work -- ensure domain,
|
|
356
356
|
* restrict, publish DNS, verify -- now happens inside the same step that
|
|
357
|
-
* reads this key (`ensureAppLettermintMail`), so unlike the
|
|
357
|
+
* reads this key (`ensureAppLettermintMail`), so unlike the earlier design
|
|
358
358
|
* this rework replaces, no separate domain-id scope key is recorded: the
|
|
359
359
|
* domain is looked up by name fresh each apply. Renamed from
|
|
360
|
-
*
|
|
360
|
+
* the earlier mail domain key.
|
|
361
361
|
*/
|
|
362
362
|
const MAIL_DOMAIN_SCOPE_KEY = 'mail_domain';
|
|
363
363
|
/**
|
|
@@ -770,6 +770,8 @@ export class FoundryStepExecutor {
|
|
|
770
770
|
return this.zitadelLoginHost(action, org, context);
|
|
771
771
|
case 'zitadel:app':
|
|
772
772
|
return this.zitadelApp(action, org, context);
|
|
773
|
+
case 'zitadel:id-token-userinfo':
|
|
774
|
+
return this.zitadelIdTokenUserinfo(action, org, context);
|
|
773
775
|
case 'zitadel:customers':
|
|
774
776
|
return this.zitadelCustomers(org, context);
|
|
775
777
|
case 'coolify:app':
|
|
@@ -1312,6 +1314,44 @@ export class FoundryStepExecutor {
|
|
|
1312
1314
|
}
|
|
1313
1315
|
return { resourceId: userId };
|
|
1314
1316
|
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Switches on "user info inside the ID token" for each live sign-in app
|
|
1319
|
+
* the planner named. An app's marker (its ZITADEL app id) is recorded and
|
|
1320
|
+
* persisted only after its call resolved to a boolean, so a call that throws
|
|
1321
|
+
* or resolves to anything else leaves no marker and the next plan names the
|
|
1322
|
+
* app again. The error names ids only,
|
|
1323
|
+
* never the cause.
|
|
1324
|
+
*/
|
|
1325
|
+
async zitadelIdTokenUserinfo(action, org, context) {
|
|
1326
|
+
const update = this.ports.zitadel.updateOidcAppIdTokenUserinfoAssertion?.bind(this.ports.zitadel);
|
|
1327
|
+
// The planner plans nothing without the method; a stale plan does nothing.
|
|
1328
|
+
if (!update)
|
|
1329
|
+
return {};
|
|
1330
|
+
const recorded = context.recordedIds();
|
|
1331
|
+
const zitadelOrgId = recorded.zitadel_org;
|
|
1332
|
+
const projectId = recorded.zitadel_project;
|
|
1333
|
+
if (!zitadelOrgId || !projectId) {
|
|
1334
|
+
throw new Error(`zitadel:id-token-userinfo for ${org.slug}: no ZITADEL org and project recorded yet -- the issuer must be provisioned first`);
|
|
1335
|
+
}
|
|
1336
|
+
for (const appKey of action.desired.apps) {
|
|
1337
|
+
const appId = recorded[`zitadel:app:${appKey}`];
|
|
1338
|
+
if (!appId)
|
|
1339
|
+
continue;
|
|
1340
|
+
let done;
|
|
1341
|
+
try {
|
|
1342
|
+
done = await update(zitadelOrgId, projectId, appId);
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
throw new Error(`zitadel:id-token-userinfo for ${org.slug}: switching on user info in the ID token failed for app '${appKey}' (${appId})`);
|
|
1346
|
+
}
|
|
1347
|
+
// A client that answers every property (`packages/local`) resolves to something else: no marker.
|
|
1348
|
+
if (typeof done !== 'boolean')
|
|
1349
|
+
continue;
|
|
1350
|
+
context.recordScopeId(zitadelAppIdTokenUserinfoAppliedScopeKey(appKey), appId);
|
|
1351
|
+
await context.persistScopeIds();
|
|
1352
|
+
}
|
|
1353
|
+
return {};
|
|
1354
|
+
}
|
|
1315
1355
|
/**
|
|
1316
1356
|
* `plan/diff.ts`'s `planLoginHost` (#267): adds the org's own company
|
|
1317
1357
|
* login host (`action.desired.host`) to the ZITADEL instance's trusted
|
|
@@ -1706,6 +1746,8 @@ export class FoundryStepExecutor {
|
|
|
1706
1746
|
// never something an app's env needs.
|
|
1707
1747
|
context.recordScopeId(clientIdScopeKey, created.clientId);
|
|
1708
1748
|
}
|
|
1749
|
+
// Made with the setting on (providers 0.9.0): nothing to repair on this id.
|
|
1750
|
+
context.recordScopeId(zitadelAppIdTokenUserinfoAppliedScopeKey(action.key), created.appId);
|
|
1709
1751
|
return { resourceId: created.appId };
|
|
1710
1752
|
}
|
|
1711
1753
|
/**
|
|
@@ -2453,7 +2495,7 @@ export class FoundryStepExecutor {
|
|
|
2453
2495
|
*
|
|
2454
2496
|
* Mint rule ("Credential model"): the project is healthy when the app env
|
|
2455
2497
|
* holds `LETTERMINT_API_TOKEN` and Lettermint lists exactly one project
|
|
2456
|
-
* named `<slug>-app` -- unlike
|
|
2498
|
+
* named `<slug>-app` -- unlike the earlier provider's key, a Lettermint project's name
|
|
2457
2499
|
* carries no domain id, since a domain switch is handled entirely by
|
|
2458
2500
|
* {@link LettermintPort.restrictDomainToProjects} below and never needs a
|
|
2459
2501
|
* fresh project. Otherwise a fresh project is created and its token
|
|
@@ -3771,6 +3813,8 @@ export class FoundryStepExecutor {
|
|
|
3771
3813
|
await this.ports.coolify.raw('POST', `/applications/${appId}/stop`);
|
|
3772
3814
|
return { resourceId: appId };
|
|
3773
3815
|
}
|
|
3816
|
+
// Refused before anything is created, like the missing-stack refusal below.
|
|
3817
|
+
const sfuUrls = resolveCallsSfuUrls(this.ports.callsSfuUrls);
|
|
3774
3818
|
const recorded = context.recordedIds();
|
|
3775
3819
|
const synapseId = recorded[CALLS_SYNAPSE_SCOPE_KEY];
|
|
3776
3820
|
const masId = recorded[CALLS_MAS_SCOPE_KEY];
|
|
@@ -3832,8 +3876,8 @@ export class FoundryStepExecutor {
|
|
|
3832
3876
|
await this.ensureLogVolume(app.appId, org, app.existed);
|
|
3833
3877
|
const bindingSecret = await this.ensureCallsBindingSecret(app.appId);
|
|
3834
3878
|
const envs = [
|
|
3835
|
-
{ key: 'LIVEKIT_URL', value:
|
|
3836
|
-
{ key: 'LIVEKIT_PUBLIC_URL', value:
|
|
3879
|
+
{ key: 'LIVEKIT_URL', value: sfuUrls.privateUrl },
|
|
3880
|
+
{ key: 'LIVEKIT_PUBLIC_URL', value: sfuUrls.publicUrl },
|
|
3837
3881
|
{ key: 'LIVEKIT_API_KEY', value: sfu.apiKey },
|
|
3838
3882
|
{ key: 'LIVEKIT_API_SECRET', value: sfu.apiSecret },
|
|
3839
3883
|
{ key: 'MAS_URL', value: `http://${masHost}:8080/` },
|
|
@@ -5144,7 +5188,7 @@ export class FoundryStepExecutor {
|
|
|
5144
5188
|
* upsert `publishMailDnsRecords` above uses for Stalwart's own records --
|
|
5145
5189
|
* except an existing `_dmarc` record is never overwritten (a stricter
|
|
5146
5190
|
* policy an org already set, `p=quarantine`/`p=reject`, should not be
|
|
5147
|
-
* relaxed back to Lettermint's own default).
|
|
5191
|
+
* relaxed back to Lettermint's own default). The earlier provider's domain also
|
|
5148
5192
|
* carried an apex `SPF` TXT record that had to be merged into whatever SPF
|
|
5149
5193
|
* record already existed there (RFC 7208: two `v=spf1` TXT records on one
|
|
5150
5194
|
* name is a permerror); Lettermint's own DNS records
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { computePlanDetail, toPlan } from './diff.js';
|
|
|
5
5
|
export type { StepSystem } from './step-system.js';
|
|
6
6
|
export { stepSystemOf } from './step-system.js';
|
|
7
7
|
export type { PlatformPlanDetail } from './platform-diff.js';
|
|
8
|
-
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, } from './platform-diff.js';
|
|
8
|
+
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
9
9
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
|
|
10
10
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
11
11
|
export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
|
@@ -15,7 +15,7 @@ export { AdoptRefusedError } from './adopt.js';
|
|
|
15
15
|
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan-apply.js';
|
|
16
16
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
17
17
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
18
|
-
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './platform-providers.js';
|
|
18
|
+
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './platform-providers.js';
|
|
19
19
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
|
|
20
20
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
21
21
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
@@ -23,7 +23,7 @@ export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from '
|
|
|
23
23
|
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
24
24
|
export { createZitadelProjectRoles } from './zitadel-project-roles.js';
|
|
25
25
|
export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './platform-sfu.js';
|
|
26
|
-
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
26
|
+
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
27
27
|
export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './mail-copy.js';
|
|
28
28
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
29
29
|
export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './providers-readers.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
2
2
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
3
3
|
export { stepSystemOf } from './step-system.js';
|
|
4
|
-
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, } from './platform-diff.js';
|
|
4
|
+
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
5
5
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
6
6
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
7
7
|
export { AdoptRefusedError } from './adopt.js';
|
|
@@ -12,7 +12,7 @@ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEME
|
|
|
12
12
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
13
13
|
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
14
14
|
export { createZitadelProjectRoles } from './zitadel-project-roles.js';
|
|
15
|
-
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
15
|
+
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
16
16
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
17
17
|
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
18
18
|
export { PlatformBrokerPort } from './broker-entries.js';
|
package/dist/plan/live.d.ts
CHANGED
|
@@ -23,9 +23,9 @@ export interface LiveResource {
|
|
|
23
23
|
/**
|
|
24
24
|
* `'sfu'` (#170) has no reader wired below -- there is no cheap way to
|
|
25
25
|
* probe the calls SFU VM's own state over SSH during planning, so
|
|
26
|
-
* `platform-diff.ts`'s `planSfu`
|
|
27
|
-
* `DeclaredPlatform.sfu.enabled` is true, rather than
|
|
28
|
-
* live resource the way every other system here does. It is still part of
|
|
26
|
+
* `platform-diff.ts`'s `planSfu` plans its step from a recorded
|
|
27
|
+
* fingerprint when `DeclaredPlatform.sfu.enabled` is true, rather than
|
|
28
|
+
* diffing against a live resource the way every other system here does. It is still part of
|
|
29
29
|
* this union so `StepAction.system` (`diff.ts`) can name it. `'edge'`
|
|
30
30
|
* (#317) is the same: a edge key has no live lookup of its own, the same
|
|
31
31
|
* shape `ai`/`files` entry keys already follow.
|
|
@@ -361,6 +361,13 @@ export interface FoundryPlanApplyOptions {
|
|
|
361
361
|
* before zitadel-login's binding can be written.
|
|
362
362
|
*/
|
|
363
363
|
loginHostBindings?: boolean;
|
|
364
|
+
/**
|
|
365
|
+
* True when the host's ZITADEL client has `updateOidcAppIdTokenUserinfoAssertion`
|
|
366
|
+
* (`@wtfalch/providers` at the version that has it). Optional, and false
|
|
367
|
+
* without it: no org plans the step that switches on user info in the ID
|
|
368
|
+
* token of its existing sign-in apps.
|
|
369
|
+
*/
|
|
370
|
+
zitadelIdTokenUserinfo?: boolean;
|
|
364
371
|
/**
|
|
365
372
|
* F5: true when the host's executor was built with a `broker` port.
|
|
366
373
|
* Optional, and false without it: each entry's broker entry is then
|
|
@@ -461,6 +468,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
461
468
|
private readonly agora;
|
|
462
469
|
private readonly jobsScheduler;
|
|
463
470
|
private readonly loginHostBindings;
|
|
471
|
+
private readonly zitadelIdTokenUserinfo;
|
|
464
472
|
private readonly broker;
|
|
465
473
|
private readonly edgeWorkers;
|
|
466
474
|
private readonly hostFiles;
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -121,6 +121,7 @@ export class FoundryPlanApply {
|
|
|
121
121
|
agora;
|
|
122
122
|
jobsScheduler;
|
|
123
123
|
loginHostBindings;
|
|
124
|
+
zitadelIdTokenUserinfo;
|
|
124
125
|
broker;
|
|
125
126
|
edgeWorkers;
|
|
126
127
|
hostFiles;
|
|
@@ -142,6 +143,7 @@ export class FoundryPlanApply {
|
|
|
142
143
|
this.agora = options.agora;
|
|
143
144
|
this.jobsScheduler = options.jobsScheduler ?? false;
|
|
144
145
|
this.loginHostBindings = options.loginHostBindings ?? false;
|
|
146
|
+
this.zitadelIdTokenUserinfo = options.zitadelIdTokenUserinfo ?? false;
|
|
145
147
|
this.broker = options.broker ?? false;
|
|
146
148
|
this.edgeWorkers = options.edgeWorkers ?? false;
|
|
147
149
|
this.hostFiles = options.filesOrganisation ?? false;
|
|
@@ -876,6 +878,7 @@ export class FoundryPlanApply {
|
|
|
876
878
|
const platformHostnames = await this.platformServiceHostnames();
|
|
877
879
|
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, {
|
|
878
880
|
loginHostBindings: this.loginHostBindings,
|
|
881
|
+
zitadelIdTokenUserinfo: this.zitadelIdTokenUserinfo,
|
|
879
882
|
broker: this.broker,
|
|
880
883
|
edgeWorkers: this.edgeWorkers,
|
|
881
884
|
otherSlugs,
|
|
@@ -1005,9 +1008,13 @@ export class FoundryPlanApply {
|
|
|
1005
1008
|
store.getServiceIds(),
|
|
1006
1009
|
]);
|
|
1007
1010
|
const sfuBlocked = await this.sfuBlockedReason(platform);
|
|
1011
|
+
const callsOrgSlugs = (await this.store.list())
|
|
1012
|
+
.filter((org) => org.calls.enabled)
|
|
1013
|
+
.map((org) => org.slug);
|
|
1008
1014
|
const detail = computePlatformPlanDetail(platform, live, {
|
|
1009
1015
|
jobsObservable: readers.coolify !== undefined,
|
|
1010
1016
|
sfuBlocked,
|
|
1017
|
+
callsOrgSlugs,
|
|
1011
1018
|
recordedIds: serviceIds,
|
|
1012
1019
|
blocked: (await executor.blockedSteps?.(platform, serviceIds)) ?? {},
|
|
1013
1020
|
drifted: (await executor.driftedSteps?.(platform, serviceIds)) ?? {},
|
|
@@ -1301,6 +1308,7 @@ const STEP_FIELDS = {
|
|
|
1301
1308
|
'zitadel:issuer': 'issuer.usesDomain',
|
|
1302
1309
|
'zitadel:owner': 'owners',
|
|
1303
1310
|
'zitadel:customers': 'hosting.entries.uses.auth',
|
|
1311
|
+
'zitadel:id-token-userinfo': 'hosting.entries.uses.auth',
|
|
1304
1312
|
'coolify:broker-entry': 'hosting.entries.uses.auth',
|
|
1305
1313
|
'coolify:mail': 'mail',
|
|
1306
1314
|
'coolify:chat': 'chat.usesDomain',
|