@wtfalch/foundry 0.16.13 → 0.16.14

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.
@@ -125,6 +125,18 @@ export declare const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = "archon_applied_
125
125
  * between is repaired by the next one.
126
126
  */
127
127
  export declare const PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY = "archon_gate_closed";
128
+ /**
129
+ * The ZITADEL SMTP provider (Lettermint) `provisionPlatformAuth` configured on
130
+ * the platform's instance, recorded as the provider id. Without it ZITADEL
131
+ * cannot send the owner's invite, so a platform applied before it existed
132
+ * plans the auth step again.
133
+ */
134
+ export declare const PLATFORM_AUTH_SMTP_SCOPE_KEY = "zitadel_platform_smtp_provider_id";
135
+ /**
136
+ * The platform owner's ZITADEL user id, recorded once the invite email was
137
+ * requested, so the invite goes out once. `none` when no owner is declared.
138
+ */
139
+ export declare const PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY = "zitadel_platform_owner_invite_user_id";
128
140
  export declare const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = "zitadel_platform_admin_user_id";
129
141
  export declare const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = "zitadel_platform_admin_user_undelivered";
130
142
  /** #283: the created instance's id, recorded once its owner token is delivered. */
@@ -145,6 +145,18 @@ export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_a
145
145
  * between is repaired by the next one.
146
146
  */
147
147
  export const PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY = 'archon_gate_closed';
148
+ /**
149
+ * The ZITADEL SMTP provider (Lettermint) `provisionPlatformAuth` configured on
150
+ * the platform's instance, recorded as the provider id. Without it ZITADEL
151
+ * cannot send the owner's invite, so a platform applied before it existed
152
+ * plans the auth step again.
153
+ */
154
+ export const PLATFORM_AUTH_SMTP_SCOPE_KEY = 'zitadel_platform_smtp_provider_id';
155
+ /**
156
+ * The platform owner's ZITADEL user id, recorded once the invite email was
157
+ * requested, so the invite goes out once. `none` when no owner is declared.
158
+ */
159
+ export const PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY = 'zitadel_platform_owner_invite_user_id';
148
160
  export const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = 'zitadel_platform_admin_user_id';
149
161
  export const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_admin_user_undelivered';
150
162
  /** #283: the created instance's id, recorded once its owner token is delivered. */
@@ -1131,6 +1143,10 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
1131
1143
  brokerApplied &&
1132
1144
  brokerDeployed &&
1133
1145
  loginClientApplied &&
1146
+ // Mail and the owner's invite: a platform applied before they existed
1147
+ // plans the step again, which only adds them.
1148
+ Boolean(recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY]) &&
1149
+ Boolean(recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY]) &&
1134
1150
  // The ZITADEL run options reach the container only through the auth
1135
1151
  // step's redeploy, so a platform recorded before them still plans it.
1136
1152
  recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS) {
@@ -13,13 +13,32 @@ import { type CallsSfuPlatformDeps } from './platform-sfu.js';
13
13
  * positions, so every #167-era caller of {@link FoundryPlatformStepExecutor}
14
14
  * (zone only) keeps compiling with this third argument simply omitted.
15
15
  */
16
+ /**
17
+ * The SMTP call `provisionPlatformAuth` makes on the instance's ZITADEL.
18
+ * `@wtfalch/providers` gains it in 0.7.2; stated here so foundry compiles
19
+ * against 0.7.1 and tests use fakes.
20
+ */
21
+ export interface PlatformZitadelSmtp {
22
+ ensureSmtpProvider(config: {
23
+ host: string;
24
+ user: string;
25
+ password: string;
26
+ senderAddress: string;
27
+ senderName: string;
28
+ tls: boolean;
29
+ description: string;
30
+ }): Promise<{
31
+ id: string;
32
+ changed: boolean;
33
+ }>;
34
+ }
16
35
  export interface PlatformAuthDeps {
17
36
  coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
18
37
  cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'
19
38
  /** #197: fetches the tunnel's connector token for {@link ensureCloudflaredConnector}. */
20
39
  | 'getTunnelToken'>;
21
40
  /** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
22
- zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats'>;
41
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'inviteOrgOwner'> & PlatformZitadelSmtp;
23
42
  /**
24
43
  * Builds the ZITADEL port for the new instance from its issuer URL
25
44
  * (`https://<auth.hostname>`). When set it is used instead of
@@ -198,7 +217,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
198
217
  private readonly ai?;
199
218
  private readonly archon?;
200
219
  private readonly backup?;
201
- constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list'>, options?: FoundryPlatformStepExecutorOptions);
220
+ constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list' | 'get'>, options?: FoundryPlatformStepExecutorOptions);
202
221
  blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
203
222
  auth?: string;
204
223
  authExecutorMissing?: boolean;
@@ -6,7 +6,7 @@ import { redactSecrets } from '../calls-sfu-deploy-support.js';
6
6
  import { ensureCloudflaredConnector } from './cloudflared-connector.js';
7
7
  import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
8
8
  import { coolifyDomains } from './hostnames.js';
9
- import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
9
+ import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
10
10
  import { applyCallsSfuPlatformStep } from './platform-sfu.js';
11
11
  import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
12
12
  /** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
@@ -302,7 +302,8 @@ export class FoundryPlatformStepExecutor {
302
302
  if (!this.auth) {
303
303
  throw new Error('platform:auth needs FoundryPlatformStepExecutor to be constructed with its auth dependencies (#168)');
304
304
  }
305
- return provisionPlatformAuth(context, this.auth);
305
+ const company = await this.declaredOrgStore.get(context.platform.ownerOrgId);
306
+ return provisionPlatformAuth(context, this.auth, company?.owners ?? []);
306
307
  }
307
308
  if (action.system === 'coolify' && action.kind === 'app' && action.key === PLATFORM_FILES_KEY) {
308
309
  if (!this.files) {
@@ -856,7 +857,7 @@ async function liveLoginClientTokenHash(coolify, appId) {
856
857
  * #197: a `cloudflared` connector for this tunnel is run
857
858
  * ({@link ensureCloudflaredConnector}), so `hostname` answers.
858
859
  */
859
- async function provisionPlatformAuth(context, deps) {
860
+ async function provisionPlatformAuth(context, deps, owners = []) {
860
861
  const { platform } = context;
861
862
  const hostname = platform.auth.hostname;
862
863
  const gitRef = platform.auth.gitRef;
@@ -1403,8 +1404,62 @@ async function provisionPlatformAuth(context, deps) {
1403
1404
  // own doc comment, and `authLoginClientDrifted`'s, for why.
1404
1405
  await record(PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, loginClientRecordValue(loginClientUserId, loginClientPatId, loginClientToken));
1405
1406
  }
1407
+ // ---- ZITADEL mail and the owner's invite ----------------------------------
1408
+ // The token is the held key, else the broker's live env. Absent, nothing
1409
+ // here can run; `authBrokerBlockedReason` already reports it by name.
1410
+ const mailToken = heldMailKey || (await currentEnvValue(deps.coolify, brokerAppId, 'LETTERMINT_API_TOKEN'));
1411
+ if (mailToken) {
1412
+ const mailZitadel = deps.zitadelForInstance?.(`https://${hostname}`) ?? deps.zitadel;
1413
+ try {
1414
+ // Runs every time the step does (idempotent), so a rotated token lands.
1415
+ const smtp = await mailZitadel.ensureSmtpProvider({
1416
+ host: PLATFORM_AUTH_SMTP_HOST,
1417
+ user: PLATFORM_AUTH_SMTP_USER,
1418
+ password: mailToken,
1419
+ senderAddress: `noreply@${platform.apex}`,
1420
+ senderName: PLATFORM_AUTH_MAIL_SENDER_NAME,
1421
+ tls: true,
1422
+ description: PLATFORM_AUTH_SMTP_DESCRIPTION,
1423
+ });
1424
+ await record(PLATFORM_AUTH_SMTP_SCOPE_KEY, smtp.id);
1425
+ }
1426
+ catch {
1427
+ throw new Error(`platform:auth: could not configure ZITADEL's SMTP provider for ${hostname}; the cause is left out because it can carry the Lettermint key`);
1428
+ }
1429
+ if (!context.recordedIds()[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY]) {
1430
+ const owner = owners[0];
1431
+ if (!owner) {
1432
+ // No owner declared: nothing to invite. Recorded so the step is not replanned for it.
1433
+ await record(PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, 'none');
1434
+ }
1435
+ else {
1436
+ const ownerOrgId = context.recordedIds()[PLATFORM_ZITADEL_ORG_SCOPE_KEY] ??
1437
+ (await mailZitadel.findOrCreateOrg(PLATFORM_AUTH_ORG_NAME)).org.id;
1438
+ await record(PLATFORM_ZITADEL_ORG_SCOPE_KEY, ownerOrgId);
1439
+ let invitedUserId;
1440
+ try {
1441
+ ({ userId: invitedUserId } = await mailZitadel.inviteOrgOwner(ownerOrgId, {
1442
+ givenName: owner.givenName,
1443
+ familyName: owner.familyName,
1444
+ email: owner.email,
1445
+ }));
1446
+ }
1447
+ catch {
1448
+ throw new Error(`platform:auth: could not invite the platform owner into ZITADEL org ${ownerOrgId}; the cause is left out because it can carry the owner's email`);
1449
+ }
1450
+ await record(PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, invitedUserId);
1451
+ }
1452
+ }
1453
+ }
1406
1454
  return { resourceId: zitadelAppId };
1407
1455
  }
1456
+ /** Lettermint's SMTP relay, as ZITADEL's SMTP provider wants it: `host:port`, STARTTLS. */
1457
+ const PLATFORM_AUTH_SMTP_HOST = 'smtp.lettermint.co:587';
1458
+ const PLATFORM_AUTH_SMTP_USER = 'lettermint';
1459
+ /** Marks the provider as foundry's, so `ensureSmtpProvider` finds it again. */
1460
+ const PLATFORM_AUTH_SMTP_DESCRIPTION = 'foundry:platform:lettermint';
1461
+ /** The same name the broker's `from` uses (`Company Included <noreply@apex>`). */
1462
+ const PLATFORM_AUTH_MAIL_SENDER_NAME = 'Company Included';
1408
1463
  // ---------------------------------------------------------------------------
1409
1464
  // #169: the platform's own mail server (Stalwart)
1410
1465
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.16.13",
3
+ "version": "0.16.14",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "@wtfalch/audit": "0.6.0",
44
44
  "@wtfalch/auth": "0.16.0",
45
45
  "@wtfalch/email": "^0.17.0",
46
- "@wtfalch/providers": "0.6.0",
46
+ "@wtfalch/providers": "0.7.2",
47
47
  "ssh2": "1.17.0",
48
48
  "zod": "4.6.5"
49
49
  },