@wtfalch/foundry 0.54.0 → 0.56.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/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +9 -2
- package/dist/plan/executor.d.ts +5 -0
- package/dist/plan/executor.js +34 -4
- package/dist/plan/login-host.d.ts +8 -0
- package/dist/plan/login-host.js +14 -0
- package/dist/plan/own-diff.d.ts +2 -0
- package/dist/plan/own-diff.js +13 -1
- package/dist/plan/plan-apply.d.ts +2 -0
- package/dist/plan/plan-apply.js +1 -0
- package/dist/plan/platform-providers.d.ts +12 -1
- package/dist/plan/platform-providers.js +25 -1
- package/dist/plan/signin-mail.d.ts +83 -0
- package/dist/plan/signin-mail.js +69 -0
- package/package.json +1 -1
package/dist/plan/diff.d.ts
CHANGED
|
@@ -101,6 +101,8 @@ export interface PlanOrgOptions {
|
|
|
101
101
|
workerAppDbBackupConfigured?: boolean;
|
|
102
102
|
/** The executor can store the sign-in database's CA and certificate (`FoundryStepExecutor.ownSigninDbKeysPuttable`). Absent: true. */
|
|
103
103
|
ownSigninDbKeysPuttable?: boolean;
|
|
104
|
+
/** The executor's company ZITADEL port can write the sign-in mail (`FoundryStepExecutor.ownSigninMailWritable`). Absent: true. */
|
|
105
|
+
ownSigninMailWritable?: boolean;
|
|
104
106
|
/** The host supplies the backup bucket the sign-in database dumps into (`FoundryStepExecutor.ownSigninDbBackupConfigured`). Absent: true. */
|
|
105
107
|
ownSigninDbBackupConfigured?: boolean;
|
|
106
108
|
/** The own jobs admin token's held-key ref holds a value. */
|
package/dist/plan/diff.js
CHANGED
|
@@ -18,7 +18,7 @@ import { hasWildcardGate, planGates } from './gates.js';
|
|
|
18
18
|
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
19
19
|
import { APP_GONE_KIND } from './live.js';
|
|
20
20
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
21
|
-
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, agoraIssuerHostFor, clientOrganizationsState, companyLoginTunnelHost, customersClientOrganizations, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
21
|
+
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, agoraIssuerHostFor, clientOrganizationsState, companyLoginTunnelHost, customersClientOrganizations, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, ownCustomersAppState, pendingRemovalHosts, } from './login-host.js';
|
|
22
22
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
23
23
|
import { mailStoreSwitchChange } from './mail-store.js';
|
|
24
24
|
import { planMcp } from './mcp.js';
|
|
@@ -214,6 +214,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
214
214
|
now: (options.now ?? Date.now)(),
|
|
215
215
|
signinDbKeysPuttable: options.ownSigninDbKeysPuttable ?? true,
|
|
216
216
|
signinDbBackupConfigured: options.ownSigninDbBackupConfigured ?? true,
|
|
217
|
+
signinMailWritable: options.ownSigninMailWritable ?? true,
|
|
217
218
|
});
|
|
218
219
|
if (ownAuth.step)
|
|
219
220
|
plannedSteps.push(ownAuth.step);
|
|
@@ -1394,8 +1395,13 @@ function planOwnLoginHost(org, recordedIds, gates) {
|
|
|
1394
1395
|
const none = { step: null, blocked: null };
|
|
1395
1396
|
const mapOwed = clientOrganizationsOwed(org, recordedIds, true);
|
|
1396
1397
|
// Nothing wanted and nothing owed.
|
|
1397
|
-
|
|
1398
|
+
// A customers entry's app owes its base URI (the company's login host) until the marker names it.
|
|
1399
|
+
const customersOwed = Boolean(recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY]) &&
|
|
1400
|
+
ownCustomersAppState(org, recordedIds) !==
|
|
1401
|
+
(recordedIds[OWN_CUSTOMERS_BASE_URI_SCOPE_KEY] ?? '');
|
|
1402
|
+
if (!inAppSignIn(org) && !recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] && !mapOwed && !customersOwed) {
|
|
1398
1403
|
return none;
|
|
1404
|
+
}
|
|
1399
1405
|
// The company's own sign-in is reported on its own line (`coolify:own-auth`).
|
|
1400
1406
|
if (!ownAuthApplied(org, recordedIds))
|
|
1401
1407
|
return none;
|
|
@@ -1411,6 +1417,7 @@ function planOwnLoginHost(org, recordedIds, gates) {
|
|
|
1411
1417
|
if ((recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1412
1418
|
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
|
|
1413
1419
|
!mapOwed &&
|
|
1420
|
+
!customersOwed &&
|
|
1414
1421
|
pendingRemovalHosts(recordedIds).length === 0) {
|
|
1415
1422
|
return { step: null, blocked: waiting };
|
|
1416
1423
|
}
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -2021,6 +2021,11 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2021
2021
|
bouleArchonCredentialAvailable(): boolean;
|
|
2022
2022
|
/** Whether the held keys can store a new company's sign-in database CA and certificate (#746). */
|
|
2023
2023
|
ownSigninDbKeysPuttable(): boolean;
|
|
2024
|
+
/**
|
|
2025
|
+
* Whether the host says its company ZITADEL port has `ensureEmailOtpMessageText` and `ensureLabelPolicy`
|
|
2026
|
+
* (`provision.auth.signinMailWritable`). The port is a lazy proxy that answers any name, so it cannot be probed.
|
|
2027
|
+
*/
|
|
2028
|
+
ownSigninMailWritable(): boolean;
|
|
2024
2029
|
/** Whether the held keys can store a Worker app database's CA and certificate. */
|
|
2025
2030
|
workerAppDbKeysPuttable(): boolean;
|
|
2026
2031
|
/** Whether the host supplies the backup bucket a Worker app database dumps into (the files host's bucket and key ports). */
|
package/dist/plan/executor.js
CHANGED
|
@@ -37,7 +37,7 @@ import { GATE_CLAIM_NAME, gateHostnamesScopeKey, gateIdRecordKey, isBehindGate }
|
|
|
37
37
|
import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, wgetProbe, } from './health-check-settings.js';
|
|
38
38
|
import { PROXY_LABEL_TOKEN_REF } from './held-key-refs.js';
|
|
39
39
|
import { appHostname, cmsStudioHostnameFor, coolifyTunnelDomains, entryHostnames, siteHostnameFor, tunnelDnsHostnames, tunnelIngressHostnames, } from './hostnames.js';
|
|
40
|
-
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
40
|
+
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
41
41
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
42
42
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, normalizeMailTarget, } from './mail-relay.js';
|
|
43
43
|
import { applyAccessTokenLifetime, applyMcpAccess, applyMcpProject } from './mcp.js';
|
|
@@ -1013,6 +1013,13 @@ export class FoundryStepExecutor {
|
|
|
1013
1013
|
ownSigninDbKeysPuttable() {
|
|
1014
1014
|
return Boolean(this.ports.ownServices?.provision.auth.heldKeys?.put);
|
|
1015
1015
|
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Whether the host says its company ZITADEL port has `ensureEmailOtpMessageText` and `ensureLabelPolicy`
|
|
1018
|
+
* (`provision.auth.signinMailWritable`). The port is a lazy proxy that answers any name, so it cannot be probed.
|
|
1019
|
+
*/
|
|
1020
|
+
ownSigninMailWritable() {
|
|
1021
|
+
return this.ports.ownServices?.provision.auth.signinMailWritable === true;
|
|
1022
|
+
}
|
|
1016
1023
|
/** Whether the held keys can store a Worker app database's CA and certificate. */
|
|
1017
1024
|
workerAppDbKeysPuttable() {
|
|
1018
1025
|
return typeof this.ports.heldKeys?.put === 'function';
|
|
@@ -2268,6 +2275,17 @@ export class FoundryStepExecutor {
|
|
|
2268
2275
|
customersAppIds.add(appId);
|
|
2269
2276
|
}
|
|
2270
2277
|
}
|
|
2278
|
+
// A company's own instance: a customers entry's app signs in on the company's login host. Without a base
|
|
2279
|
+
// URI of its own it falls back to the v1 login once in-app sign-in turns `loginV2.required` off.
|
|
2280
|
+
const ownCustomersAppIds = new Set();
|
|
2281
|
+
if (own) {
|
|
2282
|
+
for (const key of customersKeys) {
|
|
2283
|
+
const appId = recorded[`zitadel:app:${key}`];
|
|
2284
|
+
if (appId)
|
|
2285
|
+
ownCustomersAppIds.add(appId);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
const ownCustomersSet = [];
|
|
2271
2289
|
if (projectId) {
|
|
2272
2290
|
const listed = await this.ports.zitadel.listApps(zitadelOrgId, projectId);
|
|
2273
2291
|
listingComplete = listed.complete;
|
|
@@ -2277,6 +2295,11 @@ export class FoundryStepExecutor {
|
|
|
2277
2295
|
await setOidcAppLoginBaseUri(this.ports.zitadel, zitadelOrgId, projectId, app.id, sharedBaseUri);
|
|
2278
2296
|
continue;
|
|
2279
2297
|
}
|
|
2298
|
+
if (ownCustomersAppIds.has(app.id)) {
|
|
2299
|
+
await setOidcAppLoginBaseUri(this.ports.zitadel, zitadelOrgId, projectId, app.id, `https://${host}/`);
|
|
2300
|
+
ownCustomersSet.push(app.id);
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2280
2303
|
if (!boundAppIds.has(app.id))
|
|
2281
2304
|
continue;
|
|
2282
2305
|
// A company's own instance has the login host as its default; only the in-app apps move.
|
|
@@ -2317,9 +2340,11 @@ export class FoundryStepExecutor {
|
|
|
2317
2340
|
for (const app of liveApps) {
|
|
2318
2341
|
const uri = customersAppIds.has(app.id) && sharedBaseUri
|
|
2319
2342
|
? sharedBaseUri
|
|
2320
|
-
:
|
|
2321
|
-
?
|
|
2322
|
-
: app.
|
|
2343
|
+
: ownCustomersAppIds.has(app.id)
|
|
2344
|
+
? `https://${host}/`
|
|
2345
|
+
: boundAppIds.has(app.id)
|
|
2346
|
+
? baseUriOf(app.id)
|
|
2347
|
+
: app.loginBaseUri;
|
|
2323
2348
|
if (typeof uri === 'string')
|
|
2324
2349
|
currentLoginBaseUris.add(normalizedLoginBaseUri(uri));
|
|
2325
2350
|
}
|
|
@@ -2413,6 +2438,10 @@ export class FoundryStepExecutor {
|
|
|
2413
2438
|
if (orgsWritable) {
|
|
2414
2439
|
context.recordScopeId(LOGIN_CLIENT_ORGS_SCOPE_KEY, clientOrganizationsState(wantedOrgs));
|
|
2415
2440
|
}
|
|
2441
|
+
if (own) {
|
|
2442
|
+
// Written last: a failed base URI write leaves it behind and the step is planned again.
|
|
2443
|
+
context.recordScopeId(OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, ownCustomersSet.sort().join(','));
|
|
2444
|
+
}
|
|
2416
2445
|
context.recordScopeId(LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, pendingRemovalState(stillPending));
|
|
2417
2446
|
return { resourceId: host };
|
|
2418
2447
|
}
|
|
@@ -7320,6 +7349,7 @@ export class FoundryStepExecutor {
|
|
|
7320
7349
|
// empty; the login-host step writes it once the apps' clients exist.
|
|
7321
7350
|
...(inAppSignIn(org) ? { signInLogin: { bindings: null } } : {}),
|
|
7322
7351
|
mcp: orgDeclaresMcp(org),
|
|
7352
|
+
mailBranding: { companyName: org.displayName },
|
|
7323
7353
|
});
|
|
7324
7354
|
if (refChanged) {
|
|
7325
7355
|
const now = scope.recordedIds();
|
|
@@ -184,6 +184,14 @@ export declare const LOGIN_CLIENT_ORGS_SCOPE_KEY = "login_client_orgs";
|
|
|
184
184
|
* recorded. Staff clients are not here.
|
|
185
185
|
*/
|
|
186
186
|
export declare function customersClientOrganizations(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): Record<string, string>;
|
|
187
|
+
/**
|
|
188
|
+
* Scope id holding the ZITADEL app ids of an own org's customers entries whose Login V2 base URI was set to the
|
|
189
|
+
* company's login host (sorted, joined by `,`). On the company's own instance an app with no base URI of its own
|
|
190
|
+
* falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
|
|
191
|
+
*/
|
|
192
|
+
export declare const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = "own_customers_login_base_uri";
|
|
193
|
+
/** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
|
|
194
|
+
export declare function ownCustomersAppState(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): string;
|
|
187
195
|
/** {@link LOGIN_CLIENT_ORGS_SCOPE_KEY}'s value for a map. */
|
|
188
196
|
export declare function clientOrganizationsState(map: Readonly<Record<string, string>>): string;
|
|
189
197
|
/** The client ids a recorded {@link LOGIN_CLIENT_ORGS_SCOPE_KEY} value holds. */
|
package/dist/plan/login-host.js
CHANGED
|
@@ -211,6 +211,20 @@ export function customersClientOrganizations(org, recorded) {
|
|
|
211
211
|
}
|
|
212
212
|
return map;
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Scope id holding the ZITADEL app ids of an own org's customers entries whose Login V2 base URI was set to the
|
|
216
|
+
* company's login host (sorted, joined by `,`). On the company's own instance an app with no base URI of its own
|
|
217
|
+
* falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
|
|
218
|
+
*/
|
|
219
|
+
export const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = 'own_customers_login_base_uri';
|
|
220
|
+
/** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
|
|
221
|
+
export function ownCustomersAppState(org, recorded) {
|
|
222
|
+
return [...customersEntryKeys(org)]
|
|
223
|
+
.map((key) => recorded[`zitadel:app:${key}`])
|
|
224
|
+
.filter((id) => Boolean(id))
|
|
225
|
+
.sort()
|
|
226
|
+
.join(',');
|
|
227
|
+
}
|
|
214
228
|
/** {@link LOGIN_CLIENT_ORGS_SCOPE_KEY}'s value for a map. */
|
|
215
229
|
export function clientOrganizationsState(map) {
|
|
216
230
|
return Object.entries(map)
|
package/dist/plan/own-diff.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export interface PlanOwnAuthOptions {
|
|
|
51
51
|
signinDbKeysPuttable?: boolean;
|
|
52
52
|
/** The host supplies the backup bucket the sign-in database dumps into. Default true. */
|
|
53
53
|
signinDbBackupConfigured?: boolean;
|
|
54
|
+
/** The host's ZITADEL port can write the sign-in mail text and look. Default true. False: the marker is not required. */
|
|
55
|
+
signinMailWritable?: boolean;
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* An own org (`services.own`) runs its own sign-in: ZITADEL, the login app
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -17,6 +17,7 @@ import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn } from './sign-in.js';
|
|
|
17
17
|
import { signinDbBackupMissing } from './signin-db-backup.js';
|
|
18
18
|
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, signinDbCertDue } from './signin-db-tls.js';
|
|
19
19
|
import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db.js';
|
|
20
|
+
import { PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY, signinMailMarker } from './signin-mail.js';
|
|
20
21
|
/** `unmanaged` key of the one line `planOwnJobs` reports. */
|
|
21
22
|
export const OWN_JOBS_BLOCKED_KEY = 'own-jobs';
|
|
22
23
|
/**
|
|
@@ -163,6 +164,9 @@ export function planOwnAuth(options) {
|
|
|
163
164
|
recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY] &&
|
|
164
165
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
165
166
|
recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
|
|
167
|
+
// The code mail carries the company's name and design; an older apply, or a renamed company, writes it again.
|
|
168
|
+
(options.signinMailWritable === false ||
|
|
169
|
+
recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] === signinMailMarker(org.displayName)) &&
|
|
166
170
|
// An entry with `mcp` needs the broker's database and secret (auth ADR 0002).
|
|
167
171
|
(!orgDeclaresMcp(org) || recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] === '1') &&
|
|
168
172
|
(Boolean(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]) || !brokerHoldsAnEntry) &&
|
|
@@ -175,7 +179,15 @@ export function planOwnAuth(options) {
|
|
|
175
179
|
!signinDbBackupMissing(recordedIds) &&
|
|
176
180
|
// ZITADEL was started on the TLS database and never seen healthy there: the step starts it again.
|
|
177
181
|
!recordedIds[SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY]) {
|
|
178
|
-
|
|
182
|
+
// Applied and otherwise done: say why the mail stays ZITADEL's stock text, once.
|
|
183
|
+
const mailMissing = options.signinMailWritable === false &&
|
|
184
|
+
recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] !== signinMailMarker(org.displayName);
|
|
185
|
+
return {
|
|
186
|
+
step: null,
|
|
187
|
+
blocked: mailMissing
|
|
188
|
+
? `${org.slug}'s sign-in mail keeps ZITADEL's default text: the host cannot write message texts`
|
|
189
|
+
: null,
|
|
190
|
+
};
|
|
179
191
|
}
|
|
180
192
|
return {
|
|
181
193
|
blocked: null,
|
|
@@ -234,6 +234,8 @@ export interface StepExecutor {
|
|
|
234
234
|
ownSigninDbKeysPuttable?(): boolean;
|
|
235
235
|
/** Whether the host supplies the backup bucket the sign-in database dumps into (#746). Absent: true. */
|
|
236
236
|
ownSigninDbBackupConfigured?(): boolean;
|
|
237
|
+
/** Whether the host's company ZITADEL port can write the sign-in mail text and look. Absent: true. */
|
|
238
|
+
ownSigninMailWritable?(): boolean;
|
|
237
239
|
/**
|
|
238
240
|
* Whether the host's scoped ports for this own org include a files-organisation
|
|
239
241
|
* port. The planner plans no `files:organisation` step for an own org while
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1363,6 +1363,7 @@ export class FoundryPlanApply {
|
|
|
1363
1363
|
workerAppDbBackupConfigured: this.executor.workerAppDbBackupConfigured?.() ?? true,
|
|
1364
1364
|
ownSigninDbKeysPuttable: this.executor.ownSigninDbKeysPuttable?.() ?? true,
|
|
1365
1365
|
ownSigninDbBackupConfigured: this.executor.ownSigninDbBackupConfigured?.() ?? true,
|
|
1366
|
+
ownSigninMailWritable: this.executor.ownSigninMailWritable?.() ?? true,
|
|
1366
1367
|
...(this.backupGate
|
|
1367
1368
|
? {
|
|
1368
1369
|
backupStorageUuid: (await this.platformDeps?.store.getServiceIds())?.[PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY] || null,
|
|
@@ -20,6 +20,7 @@ import type { PlatformDriftedSteps, PlatformStepExecutionContext, PlatformStepEx
|
|
|
20
20
|
import { type BrokerDeployRecord, PLATFORM_ZITADEL_ORG_SCOPE_KEY } from './platform-diff.js';
|
|
21
21
|
import type { PlatformOwnerInput } from './platform-org.js';
|
|
22
22
|
import type { SigninDbBackupPort } from './signin-db-backup.js';
|
|
23
|
+
import { type PlatformZitadelMailBranding } from './signin-mail.js';
|
|
23
24
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
24
25
|
import { type PlatformGithubReleaseHookDeps } from './github-release-hook.js';
|
|
25
26
|
import { type CallsSfuPlatformDeps } from './platform-sfu.js';
|
|
@@ -134,6 +135,12 @@ export interface PlatformAuthSmtp {
|
|
|
134
135
|
password?: () => Promise<string>;
|
|
135
136
|
}
|
|
136
137
|
export interface PlatformAuthDeps {
|
|
138
|
+
/**
|
|
139
|
+
* The host's ZITADEL port can write the sign-in mail text and look (`PlatformZitadelMailBranding`,
|
|
140
|
+
* `@wtfalch/providers` 0.13.0). Only `true` writes it; otherwise a company keeps ZITADEL's stock text
|
|
141
|
+
* and the planner reports it. The port may be a proxy that answers any method name, so the host says it.
|
|
142
|
+
*/
|
|
143
|
+
signinMailWritable?: boolean;
|
|
137
144
|
/** Overrides the Lettermint SMTP relay ZITADEL is given. Absent, Lettermint exactly as before. */
|
|
138
145
|
smtp?: PlatformAuthSmtp;
|
|
139
146
|
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'> &
|
|
@@ -150,7 +157,7 @@ export interface PlatformAuthDeps {
|
|
|
150
157
|
requestHeaders?: PlatformRequestHeadersFactory;
|
|
151
158
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
152
159
|
/** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
|
|
153
|
-
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'enforceSecondFactor' | 'inviteOrgOwner' | 'ensureDomainPolicy' | 'addOrgDomain' | 'removeOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp
|
|
160
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'enforceSecondFactor' | 'inviteOrgOwner' | 'ensureDomainPolicy' | 'addOrgDomain' | 'removeOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp & Partial<PlatformZitadelMailBranding>;
|
|
154
161
|
/**
|
|
155
162
|
* Mails the owner's first-sign-in link to their personal address (the same
|
|
156
163
|
* port as `FoundryStepExecutorPorts.ownerInviteMail`). The link carries the
|
|
@@ -564,6 +571,10 @@ export interface AuthStackInput {
|
|
|
564
571
|
* database on the sign-in Postgres and `MCP_OAUTH_SECRET` before that entry reaches it. Absent: neither.
|
|
565
572
|
*/
|
|
566
573
|
mcp?: boolean;
|
|
574
|
+
/** A company's own sign-in: the code mail names this company (sender, text) and uses the platform's design. Absent: ZITADEL's stock mail. */
|
|
575
|
+
mailBranding?: {
|
|
576
|
+
companyName: string;
|
|
577
|
+
};
|
|
567
578
|
}
|
|
568
579
|
/**
|
|
569
580
|
* #168: makes `coolify:app` keyed {@link PLATFORM_AUTH_KEY} true -- a Coolify
|
|
@@ -36,6 +36,7 @@ import { ensureSignInOrg, inviteOwnerWithLink, signInOrgDomain } from './sign-in
|
|
|
36
36
|
import { SIGN_IN_BASE_PATH, SIGN_IN_BUILD_ARG_ENV, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_UPSTREAM_ENV, inAppOnSharedLogin, signInLoginNames, signInUpstream, } from './sign-in.js';
|
|
37
37
|
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY } from './signin-db-tls.js';
|
|
38
38
|
import { SIGNIN_DB_APP_SCOPE_KEY, SIGNIN_DB_TLS_SCOPE_KEY, dockerfileAppCreator, ensureSigninDbCaFile, provisionSigninDb, signinDbDsn, waitHealthy, withCallTimeout, } from './signin-db.js';
|
|
39
|
+
import { PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY, applySigninMail, signinMailMarker, signinMailName, } from './signin-mail.js';
|
|
39
40
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
40
41
|
import { githubReleaseHookPlanInputs, provisionGithubReleaseHook, } from './github-release-hook.js';
|
|
41
42
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
@@ -2531,7 +2532,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2531
2532
|
user: smtpTarget.user,
|
|
2532
2533
|
password: smtpPassword,
|
|
2533
2534
|
senderAddress: input.smtpSender,
|
|
2534
|
-
senderName:
|
|
2535
|
+
senderName: input.mailBranding
|
|
2536
|
+
? signinMailName(input.mailBranding.companyName)
|
|
2537
|
+
: PLATFORM_AUTH_MAIL_SENDER_NAME,
|
|
2535
2538
|
tls: smtpTarget.tls,
|
|
2536
2539
|
description: target.kind === 'platform'
|
|
2537
2540
|
? PLATFORM_AUTH_SMTP_DESCRIPTION
|
|
@@ -2547,6 +2550,27 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2547
2550
|
]);
|
|
2548
2551
|
throw new Error(`${label}: could not configure ZITADEL's SMTP provider for ${hostname}: ${cause}`);
|
|
2549
2552
|
}
|
|
2553
|
+
// A company's own sign-in: its name and design in the code mail. Idempotent; the marker is
|
|
2554
|
+
// recorded after both writes, so a failure halfway is repeated.
|
|
2555
|
+
if (input.mailBranding &&
|
|
2556
|
+
(deps.signinMailWritable !== true ||
|
|
2557
|
+
typeof mailZitadel.ensureEmailOtpMessageText !== 'function' ||
|
|
2558
|
+
typeof mailZitadel.ensureLabelPolicy !== 'function')) {
|
|
2559
|
+
console.info(`${label}: the host cannot write message texts, so the sign-in mail keeps ZITADEL's default text`);
|
|
2560
|
+
}
|
|
2561
|
+
else if (input.mailBranding) {
|
|
2562
|
+
try {
|
|
2563
|
+
await applySigninMail(mailZitadel, input.mailBranding.companyName);
|
|
2564
|
+
}
|
|
2565
|
+
catch (error) {
|
|
2566
|
+
const cause = redactSecrets(error instanceof Error ? error.message : String(error), [
|
|
2567
|
+
mailToken,
|
|
2568
|
+
...(smtpPassword ? [smtpPassword] : []),
|
|
2569
|
+
]);
|
|
2570
|
+
throw new Error(`${label}: could not set the sign-in mail text and look for ${hostname}: ${cause}`);
|
|
2571
|
+
}
|
|
2572
|
+
await record(PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY, signinMailMarker(input.mailBranding.companyName));
|
|
2573
|
+
}
|
|
2550
2574
|
const inviteRecorded = scope.recordedIds()[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
|
|
2551
2575
|
const inviteOrgRecorded = scope.recordedIds()[PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY];
|
|
2552
2576
|
// An invite recorded for another org (the `foundry` org, before the owner moved) is made again.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sign-in code mail of a company that runs its own sign-in: the company's name in the sender and
|
|
3
|
+
* the text, the platform's design tokens for the look. ZITADEL's email-code message is built from the
|
|
4
|
+
* instance's custom text and label policy; both are written through `PlatformZitadelMailBranding`
|
|
5
|
+
* (`@wtfalch/providers` 0.13.0), each only when it differs.
|
|
6
|
+
*
|
|
7
|
+
* The look: no company declares a design foundry can read (an `OrgTheme` lives in cms and reaches
|
|
8
|
+
* Agora, Boule and the site as data), so every company gets the login's house colours
|
|
9
|
+
* (`SIGNIN_MAIL_LABEL_POLICY`). No logo: ZITADEL takes it as a multipart upload the client does not make.
|
|
10
|
+
*/
|
|
11
|
+
/** Raise it when the text or the look changes, so an applied company plans its sign-in step again. */
|
|
12
|
+
export declare const AUTH_SIGNIN_MAIL_VERSION = "1";
|
|
13
|
+
/** `signinMailMarker` of the company name and version last written, recorded after both writes. */
|
|
14
|
+
export declare const PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY = "auth_signin_mail_applied";
|
|
15
|
+
/** The two ZITADEL calls the sign-in mail needs. `@wtfalch/providers` 0.13.0's `ZitadelClient` has both. */
|
|
16
|
+
export interface PlatformZitadelMailBranding {
|
|
17
|
+
ensureEmailOtpMessageText(language: string, text: {
|
|
18
|
+
title: string;
|
|
19
|
+
preHeader: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
greeting: string;
|
|
22
|
+
text: string;
|
|
23
|
+
buttonText: string;
|
|
24
|
+
footerText: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
changed: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
ensureLabelPolicy(policy: {
|
|
29
|
+
primaryColor: string;
|
|
30
|
+
backgroundColor: string;
|
|
31
|
+
fontColor: string;
|
|
32
|
+
warnColor: string;
|
|
33
|
+
primaryColorDark: string;
|
|
34
|
+
backgroundColorDark: string;
|
|
35
|
+
fontColorDark: string;
|
|
36
|
+
warnColorDark: string;
|
|
37
|
+
disableWatermark: boolean;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
changed: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/** What the marker holds: the version and the company name, so a rename writes the mail again. */
|
|
43
|
+
export declare function signinMailMarker(companyName: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The name as it goes into the sender and the text: one line, and no `{{`, which ZITADEL would run
|
|
46
|
+
* as a template action.
|
|
47
|
+
*/
|
|
48
|
+
export declare function signinMailName(companyName: string): string;
|
|
49
|
+
export declare const SIGNIN_MAIL_LANGUAGE = "en";
|
|
50
|
+
/**
|
|
51
|
+
* English text. `{{.OTP}}` is the code variable of this message (ZITADEL's `otp.go` sets it; the link
|
|
52
|
+
* behind the button carries the code as `{{.Code}}` from the login's `urlTemplate`). No greeting by
|
|
53
|
+
* name: the login fills names from the email address, so it would read "Wtfalch Wtfalch".
|
|
54
|
+
*/
|
|
55
|
+
export declare function signinMailText(companyName: string): {
|
|
56
|
+
title: string;
|
|
57
|
+
preHeader: string;
|
|
58
|
+
subject: string;
|
|
59
|
+
greeting: string;
|
|
60
|
+
text: string;
|
|
61
|
+
buttonText: string;
|
|
62
|
+
footerText: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The login's house colours as ZITADEL's label policy. The login reads the instance label policy for
|
|
66
|
+
* its own page colours and falls back to these when a value is empty (`login/src/helpers/colors.ts`
|
|
67
|
+
* in wtfalch/auth), so writing them keeps the sign-in window as it is and makes the mail match it.
|
|
68
|
+
*/
|
|
69
|
+
export declare const SIGNIN_MAIL_LABEL_POLICY: {
|
|
70
|
+
readonly primaryColor: "#B23A0C";
|
|
71
|
+
readonly backgroundColor: "#F6F3EC";
|
|
72
|
+
readonly fontColor: "#1D1B16";
|
|
73
|
+
readonly warnColor: "#B3261E";
|
|
74
|
+
readonly primaryColorDark: "#FF8A4A";
|
|
75
|
+
readonly backgroundColorDark: "#17130E";
|
|
76
|
+
readonly fontColorDark: "#EDE8DD";
|
|
77
|
+
readonly warnColorDark: "#FF9C8C";
|
|
78
|
+
readonly disableWatermark: true;
|
|
79
|
+
};
|
|
80
|
+
/** Writes the text and the look, each only when it differs. Throws the client's fixed-text error. */
|
|
81
|
+
export declare function applySigninMail(zitadel: PlatformZitadelMailBranding, companyName: string): Promise<{
|
|
82
|
+
changed: boolean;
|
|
83
|
+
}>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sign-in code mail of a company that runs its own sign-in: the company's name in the sender and
|
|
3
|
+
* the text, the platform's design tokens for the look. ZITADEL's email-code message is built from the
|
|
4
|
+
* instance's custom text and label policy; both are written through `PlatformZitadelMailBranding`
|
|
5
|
+
* (`@wtfalch/providers` 0.13.0), each only when it differs.
|
|
6
|
+
*
|
|
7
|
+
* The look: no company declares a design foundry can read (an `OrgTheme` lives in cms and reaches
|
|
8
|
+
* Agora, Boule and the site as data), so every company gets the login's house colours
|
|
9
|
+
* (`SIGNIN_MAIL_LABEL_POLICY`). No logo: ZITADEL takes it as a multipart upload the client does not make.
|
|
10
|
+
*/
|
|
11
|
+
/** Raise it when the text or the look changes, so an applied company plans its sign-in step again. */
|
|
12
|
+
export const AUTH_SIGNIN_MAIL_VERSION = '1';
|
|
13
|
+
/** `signinMailMarker` of the company name and version last written, recorded after both writes. */
|
|
14
|
+
export const PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY = 'auth_signin_mail_applied';
|
|
15
|
+
/** What the marker holds: the version and the company name, so a rename writes the mail again. */
|
|
16
|
+
export function signinMailMarker(companyName) {
|
|
17
|
+
return `${AUTH_SIGNIN_MAIL_VERSION}:${signinMailName(companyName)}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The name as it goes into the sender and the text: one line, and no `{{`, which ZITADEL would run
|
|
21
|
+
* as a template action.
|
|
22
|
+
*/
|
|
23
|
+
export function signinMailName(companyName) {
|
|
24
|
+
return companyName
|
|
25
|
+
.replace(/[\p{Cc}\s]+/gu, ' ')
|
|
26
|
+
.replaceAll('{{', '{ {')
|
|
27
|
+
.replaceAll('}}', '} }')
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
export const SIGNIN_MAIL_LANGUAGE = 'en';
|
|
31
|
+
/**
|
|
32
|
+
* English text. `{{.OTP}}` is the code variable of this message (ZITADEL's `otp.go` sets it; the link
|
|
33
|
+
* behind the button carries the code as `{{.Code}}` from the login's `urlTemplate`). No greeting by
|
|
34
|
+
* name: the login fills names from the email address, so it would read "Wtfalch Wtfalch".
|
|
35
|
+
*/
|
|
36
|
+
export function signinMailText(companyName) {
|
|
37
|
+
const name = signinMailName(companyName);
|
|
38
|
+
return {
|
|
39
|
+
title: 'Your sign-in code',
|
|
40
|
+
preHeader: `Your ${name} sign-in code`,
|
|
41
|
+
subject: `Your ${name} sign-in code`,
|
|
42
|
+
greeting: 'Hello,',
|
|
43
|
+
text: 'Your code is {{.OTP}}. It works for five minutes. Or use the button to sign in on this device.',
|
|
44
|
+
buttonText: 'Sign in',
|
|
45
|
+
footerText: name,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The login's house colours as ZITADEL's label policy. The login reads the instance label policy for
|
|
50
|
+
* its own page colours and falls back to these when a value is empty (`login/src/helpers/colors.ts`
|
|
51
|
+
* in wtfalch/auth), so writing them keeps the sign-in window as it is and makes the mail match it.
|
|
52
|
+
*/
|
|
53
|
+
export const SIGNIN_MAIL_LABEL_POLICY = {
|
|
54
|
+
primaryColor: '#B23A0C',
|
|
55
|
+
backgroundColor: '#F6F3EC',
|
|
56
|
+
fontColor: '#1D1B16',
|
|
57
|
+
warnColor: '#B3261E',
|
|
58
|
+
primaryColorDark: '#FF8A4A',
|
|
59
|
+
backgroundColorDark: '#17130E',
|
|
60
|
+
fontColorDark: '#EDE8DD',
|
|
61
|
+
warnColorDark: '#FF9C8C',
|
|
62
|
+
disableWatermark: true,
|
|
63
|
+
};
|
|
64
|
+
/** Writes the text and the look, each only when it differs. Throws the client's fixed-text error. */
|
|
65
|
+
export async function applySigninMail(zitadel, companyName) {
|
|
66
|
+
const text = await zitadel.ensureEmailOtpMessageText(SIGNIN_MAIL_LANGUAGE, signinMailText(companyName));
|
|
67
|
+
const look = await zitadel.ensureLabelPolicy({ ...SIGNIN_MAIL_LABEL_POLICY });
|
|
68
|
+
return { changed: text.changed || look.changed };
|
|
69
|
+
}
|