@wtfalch/foundry 0.14.0 → 0.15.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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plan/index.d.ts +1 -1
- package/dist/plan/index.js +1 -1
- package/dist/plan/platform-diff.d.ts +8 -0
- package/dist/plan/platform-diff.js +8 -0
- package/dist/plan/platform-providers.d.ts +38 -12
- package/dist/plan/platform-providers.js +99 -16
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { createPostgresAuditLog } from './store/audit.js';
|
|
|
15
15
|
export type { FoundryStoreDb } from './store/postgres.js';
|
|
16
16
|
export { declaredOrgs, foundryAppliedState, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
17
17
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, PlanDetail, PlatformPlanDetail, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
18
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, 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, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
18
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, 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, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
19
19
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
20
20
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
21
21
|
export type { PlatformOrgClientFactories } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
|
5
5
|
export { createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
6
6
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
7
7
|
export { declaredOrgs, foundryAppliedState, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
8
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, 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, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
8
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, 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, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
9
9
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
10
10
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
11
11
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
|
3
3
|
export type { StepAction, PlannedStep, PlanDetail } from './diff.js';
|
|
4
4
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
5
5
|
export type { PlatformPlanDetail } from './platform-diff.js';
|
|
6
|
-
export { planPlatform, toPlatformPlan, 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, } from './platform-diff.js';
|
|
6
|
+
export { planPlatform, toPlatformPlan, 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, } from './platform-diff.js';
|
|
7
7
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
|
|
8
8
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
9
9
|
export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
2
2
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
3
|
-
export { planPlatform, toPlatformPlan, 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, } from './platform-diff.js';
|
|
3
|
+
export { planPlatform, toPlatformPlan, 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, } from './platform-diff.js';
|
|
4
4
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
5
5
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
6
6
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
@@ -299,6 +299,14 @@ export declare const PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY = "mail_applied_git
|
|
|
299
299
|
* own internal address, never its admin credential.
|
|
300
300
|
*/
|
|
301
301
|
export declare const PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY = "mail_internal_url";
|
|
302
|
+
/**
|
|
303
|
+
* #346: the mail ZITADEL project's id in the platform's own sign-in, the
|
|
304
|
+
* {@link PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY} pattern for mail --
|
|
305
|
+
* `provisionPlatformMail` finds or creates it and uses the id as the OIDC
|
|
306
|
+
* audience Stalwart's directory checks an access token's `aud` claim
|
|
307
|
+
* against, so no host has to register one by hand.
|
|
308
|
+
*/
|
|
309
|
+
export declare const PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY = "zitadel_platform_mail_project";
|
|
302
310
|
/**
|
|
303
311
|
* #177: the one host the platform's mail server takes SMTP/MX on, and so the
|
|
304
312
|
* `mailHost` every org's own mail step writes. `provisionPlatformMail` (the
|
|
@@ -330,6 +330,14 @@ export const PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY = 'mail_applied_git_ref';
|
|
|
330
330
|
* own internal address, never its admin credential.
|
|
331
331
|
*/
|
|
332
332
|
export const PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY = 'mail_internal_url';
|
|
333
|
+
/**
|
|
334
|
+
* #346: the mail ZITADEL project's id in the platform's own sign-in, the
|
|
335
|
+
* {@link PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY} pattern for mail --
|
|
336
|
+
* `provisionPlatformMail` finds or creates it and uses the id as the OIDC
|
|
337
|
+
* audience Stalwart's directory checks an access token's `aud` claim
|
|
338
|
+
* against, so no host has to register one by hand.
|
|
339
|
+
*/
|
|
340
|
+
export const PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY = 'zitadel_platform_mail_project';
|
|
333
341
|
/**
|
|
334
342
|
* #177: the one host the platform's mail server takes SMTP/MX on, and so the
|
|
335
343
|
* `mailHost` every org's own mail step writes. `provisionPlatformMail` (the
|
|
@@ -248,10 +248,10 @@ export declare const PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION = "foundry:platform:adm
|
|
|
248
248
|
* instance's own URL and the recovery-admin credential this step generated
|
|
249
249
|
* ({@link PlatformMailDeps.stalwartForInstance}), the same way
|
|
250
250
|
* `zitadelForInstance` is built from a freshly issued instance's issuer URL.
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* `provisionPlatformMail`'s own doc
|
|
254
|
-
* `StalwartClient.createApiKey`.
|
|
251
|
+
* {@link defaultStalwartForInstance} is the real implementation, wrapping
|
|
252
|
+
* `@wtfalch/email/postmaster`'s `openInstance`, `applyObjects` with
|
|
253
|
+
* `platformMailSettingsObjects` (see `provisionPlatformMail`'s own doc
|
|
254
|
+
* comment for why not `mailObjects`) and `StalwartClient.createApiKey`.
|
|
255
255
|
*/
|
|
256
256
|
export interface PlatformMailAdminPort {
|
|
257
257
|
/**
|
|
@@ -261,6 +261,8 @@ export interface PlatformMailAdminPort {
|
|
|
261
261
|
*/
|
|
262
262
|
applySettings(input: {
|
|
263
263
|
mailHost: string;
|
|
264
|
+
/** The platform's own apex domain, `platformMailSettingsObjects`'s `usernameDomain`. */
|
|
265
|
+
apex: string;
|
|
264
266
|
oidc: {
|
|
265
267
|
issuerUrl: string;
|
|
266
268
|
audience: string;
|
|
@@ -269,6 +271,26 @@ export interface PlatformMailAdminPort {
|
|
|
269
271
|
/** `StalwartClient.createApiKey`'s own signature. */
|
|
270
272
|
createApiKey(description: string, permissions: readonly string[] | 'inherit'): Promise<string>;
|
|
271
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* #346: the real {@link PlatformMailAdminPort}, built from the instance's URL
|
|
276
|
+
* and the recovery-admin credential `provisionPlatformMail` just read back.
|
|
277
|
+
* `stalwartForInstance` defaults to this, so a host need not build its own --
|
|
278
|
+
* the narrower settings write `provisionPlatformMail`'s own doc comment
|
|
279
|
+
* explains ("Why not `mailObjects`/`applyObjects`" as-is) instead goes
|
|
280
|
+
* through `platformMailSettingsObjects`, which `applyObjects` resolves and
|
|
281
|
+
* writes the same way `mailObjects` would.
|
|
282
|
+
*
|
|
283
|
+
* Exported only so a unit test can call it directly against a mocked
|
|
284
|
+
* `@wtfalch/email/postmaster` and check that `url`/`user`/`password` reach
|
|
285
|
+
* `openInstance` and `apex` reaches `applySettings` -- every other test
|
|
286
|
+
* reaches this function only indirectly, through an injected
|
|
287
|
+
* `stalwartForInstance` fake.
|
|
288
|
+
*/
|
|
289
|
+
export declare function defaultStalwartForInstance(input: {
|
|
290
|
+
url: string;
|
|
291
|
+
user: string;
|
|
292
|
+
password: string;
|
|
293
|
+
}): PlatformMailAdminPort;
|
|
272
294
|
/**
|
|
273
295
|
* #169: everything {@link provisionPlatformMail} needs beyond
|
|
274
296
|
* {@link PlatformStepExecutionContext}, mirroring {@link PlatformAuthDeps}'s
|
|
@@ -278,18 +300,22 @@ export interface PlatformMailDeps {
|
|
|
278
300
|
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
|
|
279
301
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'>;
|
|
280
302
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
303
|
+
* Must target the platform's own ZITADEL instance, or `zitadelForInstance`
|
|
304
|
+
* must be set. `provisionPlatformMail` finds or creates a `mail` project
|
|
305
|
+
* in it (the {@link PlatformFilesDeps}/{@link PlatformArchonDeps} pattern)
|
|
306
|
+
* and uses the project's id as the OIDC audience Stalwart's directory
|
|
307
|
+
* checks an access token's `aud` claim against -- #346, no host has to
|
|
308
|
+
* register a resource API for this by hand any more.
|
|
286
309
|
*/
|
|
287
|
-
|
|
310
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject'>;
|
|
311
|
+
/** {@link PlatformArchonDeps.zitadelForInstance}, for Stalwart's OIDC directory. */
|
|
312
|
+
zitadelForInstance?: (issuerUrl: string) => PlatformMailDeps['zitadel'];
|
|
288
313
|
/**
|
|
289
314
|
* {@link PlatformMailAdminPort}, built from the instance this step just
|
|
290
|
-
* deployed and the recovery credential it just read back.
|
|
315
|
+
* deployed and the recovery credential it just read back. Defaults to
|
|
316
|
+
* {@link defaultStalwartForInstance}.
|
|
291
317
|
*/
|
|
292
|
-
stalwartForInstance(input: {
|
|
318
|
+
stalwartForInstance?(input: {
|
|
293
319
|
url: string;
|
|
294
320
|
user: string;
|
|
295
321
|
password: string;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, randomUUID, sign, } from 'node:crypto';
|
|
2
2
|
import { isIPv4 } from 'node:net';
|
|
3
|
+
import { applyObjects, openInstance, platformMailSettingsObjects } from '@wtfalch/email/postmaster';
|
|
3
4
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
4
5
|
import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
5
6
|
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
6
7
|
import { ACCESS_POLICY_NAME, isNotFoundError, isRunningOrStarting } from './executor.js';
|
|
7
|
-
import { 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_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_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, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
|
|
8
|
+
import { 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_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_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';
|
|
8
9
|
import { applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
9
10
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
10
11
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
@@ -1389,6 +1390,32 @@ const PLATFORM_MAIL_DEPLOYED_GIT_REF_SCOPE_KEY = 'mail_deployed_git_ref';
|
|
|
1389
1390
|
* {@link PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY} records in that case.
|
|
1390
1391
|
*/
|
|
1391
1392
|
export const PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION = 'foundry:platform:admin';
|
|
1393
|
+
/**
|
|
1394
|
+
* #346: the real {@link PlatformMailAdminPort}, built from the instance's URL
|
|
1395
|
+
* and the recovery-admin credential `provisionPlatformMail` just read back.
|
|
1396
|
+
* `stalwartForInstance` defaults to this, so a host need not build its own --
|
|
1397
|
+
* the narrower settings write `provisionPlatformMail`'s own doc comment
|
|
1398
|
+
* explains ("Why not `mailObjects`/`applyObjects`" as-is) instead goes
|
|
1399
|
+
* through `platformMailSettingsObjects`, which `applyObjects` resolves and
|
|
1400
|
+
* writes the same way `mailObjects` would.
|
|
1401
|
+
*
|
|
1402
|
+
* Exported only so a unit test can call it directly against a mocked
|
|
1403
|
+
* `@wtfalch/email/postmaster` and check that `url`/`user`/`password` reach
|
|
1404
|
+
* `openInstance` and `apex` reaches `applySettings` -- every other test
|
|
1405
|
+
* reaches this function only indirectly, through an injected
|
|
1406
|
+
* `stalwartForInstance` fake.
|
|
1407
|
+
*/
|
|
1408
|
+
export function defaultStalwartForInstance(input) {
|
|
1409
|
+
const instance = openInstance({ url: input.url, secret: input.password, user: input.user });
|
|
1410
|
+
return {
|
|
1411
|
+
async applySettings({ mailHost, apex, oidc }) {
|
|
1412
|
+
await applyObjects(instance.client, platformMailSettingsObjects({ apex, mailHost, oidc }), () => { });
|
|
1413
|
+
},
|
|
1414
|
+
createApiKey: (description, permissions) => instance.client.createApiKey(description, permissions),
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
/** The ZITADEL project and resource api Stalwart's own OIDC directory checks access tokens' `aud` claim against (#346). */
|
|
1418
|
+
const PLATFORM_MAIL_ZITADEL_PROJECT_NAME = 'mail';
|
|
1392
1419
|
/**
|
|
1393
1420
|
* #169: makes `coolify:app` keyed {@link PLATFORM_MAIL_KEY} true -- Stalwart
|
|
1394
1421
|
* on Coolify with its own persistent storage, reached at
|
|
@@ -1423,8 +1450,9 @@ export const PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION = 'foundry:platform:admin';
|
|
|
1423
1450
|
* 0006), and this step provisions no Cloudflare DNS token for Stalwart's own
|
|
1424
1451
|
* ACME either. Calling it as-is would mint an outbound relay this platform
|
|
1425
1452
|
* does not want and a credential requirement this issue does not cover, so
|
|
1426
|
-
* {@link PlatformMailAdminPort.applySettings} is a narrower
|
|
1427
|
-
*
|
|
1453
|
+
* {@link PlatformMailAdminPort.applySettings} is a narrower settings write
|
|
1454
|
+
* instead, through `platformMailSettingsObjects`: the default hostname and
|
|
1455
|
+
* the OIDC directory only.
|
|
1428
1456
|
*
|
|
1429
1457
|
* Known gaps, left for review (like #168's own "the PR stays open for
|
|
1430
1458
|
* William's review"):
|
|
@@ -1432,11 +1460,6 @@ export const PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION = 'foundry:platform:admin';
|
|
|
1432
1460
|
* Stalwart's own self-signed certificate until a follow-up wires a real
|
|
1433
1461
|
* one (`mailObjects`'s `AcmeProvider`/`DnsServer` shape, minus the Resend
|
|
1434
1462
|
* relay it also carries).
|
|
1435
|
-
* - **`oidcAudience` is supplied, not resolved.** #168 mints foundry's own
|
|
1436
|
-
* ZITADEL admin machine user but registers no resource API for Stalwart's
|
|
1437
|
-
* access-token `aud` check; whoever supplies
|
|
1438
|
-
* {@link PlatformMailDeps.oidcAudience} must have registered one by hand
|
|
1439
|
-
* until that is built.
|
|
1440
1463
|
* - **The Lettermint team token the org app step reads still comes from
|
|
1441
1464
|
* wherever the host already reads it, not held keys.** Needs
|
|
1442
1465
|
* `StepExecutionContext` to carry the platform, which `plan-apply.ts`'s
|
|
@@ -1446,6 +1469,17 @@ export const PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION = 'foundry:platform:admin';
|
|
|
1446
1469
|
* instance -- `diff.ts`'s `planMail` already blocks the step until the
|
|
1447
1470
|
* platform's own mail is applied, so the fallback only ever served a
|
|
1448
1471
|
* plan recorded before that, and now fails loudly instead.)
|
|
1472
|
+
* - **A ZITADEL client's token may not carry the mail project's id in
|
|
1473
|
+
* `aud` by default.** #346's own "Human steps": confirm this on the first
|
|
1474
|
+
* live run, and if it does not, the client must request the mail
|
|
1475
|
+
* project's audience scope.
|
|
1476
|
+
* - **The admin API key is minted with `'inherit'` permissions**, not a
|
|
1477
|
+
* narrower set scoped to only what `applySettings` needs.
|
|
1478
|
+
* - **The undelivered-key guard records before `createApiKey` mints**, not
|
|
1479
|
+
* after -- so a crash between the two records an id (`admin.createApiKey`
|
|
1480
|
+
* has not even run) that no key was ever minted for. Harmless today
|
|
1481
|
+
* (the guard only ever blocks a retry and names an id to revoke by), but
|
|
1482
|
+
* worth tightening if `createApiKey` ever starts returning its own id.
|
|
1449
1483
|
*/
|
|
1450
1484
|
async function provisionPlatformMail(context, deps) {
|
|
1451
1485
|
const { platform } = context;
|
|
@@ -1610,6 +1644,57 @@ async function provisionPlatformMail(context, deps) {
|
|
|
1610
1644
|
proxied: false,
|
|
1611
1645
|
});
|
|
1612
1646
|
}
|
|
1647
|
+
// ---- the mail ZITADEL project, in the platform's sign-in -------------------
|
|
1648
|
+
// #346: found or created on every run, like the files/Archon steps' own
|
|
1649
|
+
// projects -- its id is the OIDC audience Stalwart's directory checks an
|
|
1650
|
+
// access token's `aud` claim against, so no host has to register a
|
|
1651
|
+
// resource API for this by hand.
|
|
1652
|
+
const issuer = `https://${platform.auth.hostname}`;
|
|
1653
|
+
const zitadel = deps.zitadelForInstance?.(issuer) ?? deps.zitadel;
|
|
1654
|
+
const { org } = await zitadel.findOrCreateOrg(PLATFORM_AUTH_ORG_NAME);
|
|
1655
|
+
const { project } = await zitadel.findOrCreateProject(org.id, PLATFORM_MAIL_ZITADEL_PROJECT_NAME);
|
|
1656
|
+
const recordedProjectId = recorded[PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY];
|
|
1657
|
+
const adminKeyRecorded = Boolean(recorded[PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY]);
|
|
1658
|
+
if (adminKeyRecorded && recordedProjectId !== project.id) {
|
|
1659
|
+
// #346 round 3: the OIDC-directory write below (next block) runs only
|
|
1660
|
+
// once, while the admin key is unrecorded -- so once it is recorded, a
|
|
1661
|
+
// changed project id (the project was deleted and recreated, ZITADEL was
|
|
1662
|
+
// re-bootstrapped, or `recordedProjectId` is simply missing, from a
|
|
1663
|
+
// platform applied before this change) has no other path back to a
|
|
1664
|
+
// correct audience. Re-apply the settings write with the new id, over
|
|
1665
|
+
// the same recovery-admin credential the first run reads back, and mint
|
|
1666
|
+
// no new key -- that stays gated on the admin key being unrecorded,
|
|
1667
|
+
// below. The record is written only after `applySettings` succeeds, so a
|
|
1668
|
+
// throw here leaves the old (still-required) id in place for the next
|
|
1669
|
+
// run to retry. A project deleted out of band is noticed only here, on
|
|
1670
|
+
// the next plan/apply -- the same way the files/Archon steps' own
|
|
1671
|
+
// projects are.
|
|
1672
|
+
await waitForMailInstance(`https://${hostname}${STALWART_READY_PATH}`, runtime, sleep);
|
|
1673
|
+
const recoveryEnv = (await deps.coolify.listEnvs(appId)).find((e) => e.key === 'STALWART_RECOVERY_ADMIN' && !e.is_preview);
|
|
1674
|
+
if (!recoveryEnv) {
|
|
1675
|
+
throw new Error('platform:mail: STALWART_RECOVERY_ADMIN is not set on the app; cannot authenticate to re-apply the OIDC audience');
|
|
1676
|
+
}
|
|
1677
|
+
const password = recoveryEnv.value.slice(`${STALWART_RECOVERY_USER}:`.length);
|
|
1678
|
+
try {
|
|
1679
|
+
const admin = (deps.stalwartForInstance ?? defaultStalwartForInstance)({
|
|
1680
|
+
url: `https://${hostname}`,
|
|
1681
|
+
user: STALWART_RECOVERY_USER,
|
|
1682
|
+
password,
|
|
1683
|
+
});
|
|
1684
|
+
await admin.applySettings({
|
|
1685
|
+
mailHost,
|
|
1686
|
+
apex: platform.apex,
|
|
1687
|
+
oidc: { issuerUrl: issuer, audience: project.id },
|
|
1688
|
+
});
|
|
1689
|
+
await record(PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, project.id);
|
|
1690
|
+
}
|
|
1691
|
+
catch (error) {
|
|
1692
|
+
throw redactedMailError(error, [password]);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
else if (!recordedProjectId) {
|
|
1696
|
+
await record(PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, project.id);
|
|
1697
|
+
}
|
|
1613
1698
|
// ---- OIDC directory and foundry's own admin API key, once --------------------
|
|
1614
1699
|
if (!recorded[PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY]) {
|
|
1615
1700
|
const lost = recorded[PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY];
|
|
@@ -1623,14 +1708,15 @@ async function provisionPlatformMail(context, deps) {
|
|
|
1623
1708
|
}
|
|
1624
1709
|
const password = recoveryEnv.value.slice(`${STALWART_RECOVERY_USER}:`.length);
|
|
1625
1710
|
try {
|
|
1626
|
-
const admin = deps.stalwartForInstance({
|
|
1711
|
+
const admin = (deps.stalwartForInstance ?? defaultStalwartForInstance)({
|
|
1627
1712
|
url: `https://${hostname}`,
|
|
1628
1713
|
user: STALWART_RECOVERY_USER,
|
|
1629
1714
|
password,
|
|
1630
1715
|
});
|
|
1631
1716
|
await admin.applySettings({
|
|
1632
1717
|
mailHost,
|
|
1633
|
-
|
|
1718
|
+
apex: platform.apex,
|
|
1719
|
+
oidc: { issuerUrl: issuer, audience: project.id },
|
|
1634
1720
|
});
|
|
1635
1721
|
// Recorded and persisted before the secret leaves this function: if
|
|
1636
1722
|
// delivery fails for good, the next plan reports the lost key rather
|
|
@@ -1696,9 +1782,9 @@ async function resolveMailServer(coolify, recordedUuid) {
|
|
|
1696
1782
|
}
|
|
1697
1783
|
/**
|
|
1698
1784
|
* The mail step's own reasons it cannot run yet, as fixed text: missing
|
|
1699
|
-
* dependencies
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1785
|
+
* dependencies, a floating image tag, a server with no usable address.
|
|
1786
|
+
* Skipped once the step is applied at the declared values, so a live app is
|
|
1787
|
+
* not blocked by a later Coolify change to the server's address.
|
|
1702
1788
|
*/
|
|
1703
1789
|
async function mailBlockedReason(platform, mail, recordedIds) {
|
|
1704
1790
|
if (!mail) {
|
|
@@ -1710,9 +1796,6 @@ async function mailBlockedReason(platform, mail, recordedIds) {
|
|
|
1710
1796
|
recordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY]) {
|
|
1711
1797
|
return null;
|
|
1712
1798
|
}
|
|
1713
|
-
if (!mail.oidcAudience || mail.oidcAudience.trim() === '') {
|
|
1714
|
-
return "the platform's mail server is blocked, not planned: the host supplies no oidcAudience, the ZITADEL resource id Stalwart's OIDC directory checks access tokens against";
|
|
1715
|
-
}
|
|
1716
1799
|
const tagBlocked = mailImageTagBlocked(platform.mail.gitRef);
|
|
1717
1800
|
if (tagBlocked)
|
|
1718
1801
|
return tagBlocked;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@wtfalch/audit": "0.6.0",
|
|
44
44
|
"@wtfalch/auth": "0.16.0",
|
|
45
|
-
"@wtfalch/email": "0.
|
|
45
|
+
"@wtfalch/email": "^0.17.0",
|
|
46
46
|
"@wtfalch/providers": "0.6.0",
|
|
47
47
|
"ssh2": "1.17.0",
|
|
48
48
|
"zod": "4.6.5"
|