@wtfalch/foundry 0.28.0 → 0.30.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-ssh-client.d.ts +11 -0
- package/dist/calls-sfu-ssh-client.js +65 -11
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/org.d.ts +88 -0
- package/dist/org.js +68 -0
- package/dist/plan/agora.d.ts +13 -0
- package/dist/plan/agora.js +15 -0
- package/dist/plan/boule.d.ts +27 -3
- package/dist/plan/boule.js +39 -1
- package/dist/plan/chat-matrix.d.ts +31 -2
- package/dist/plan/chat-matrix.js +40 -6
- package/dist/plan/diff.d.ts +45 -2
- package/dist/plan/diff.js +211 -16
- package/dist/plan/executor.d.ts +132 -0
- package/dist/plan/executor.js +510 -31
- package/dist/plan/index.d.ts +5 -2
- package/dist/plan/index.js +3 -1
- package/dist/plan/plan-apply.js +1 -0
- package/dist/plan/platform-diff.d.ts +9 -0
- package/dist/plan/platform-diff.js +15 -3
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-org.js +4 -1
- package/dist/plan/platform-providers.d.ts +1 -1
- package/dist/plan/platform-providers.js +79 -22
- package/dist/plan/providers-readers.d.ts +15 -2
- package/dist/plan/providers-readers.js +44 -6
- package/dist/plan/step-system.js +1 -0
- package/dist/plan/version-status.d.ts +200 -0
- package/dist/plan/version-status.js +213 -0
- package/dist/plan/zitadel-login-policy.d.ts +23 -0
- package/dist/plan/zitadel-login-policy.js +103 -0
- package/dist/plan/zitadel-machine-user.d.ts +1 -1
- package/dist/plan/zitadel-machine-user.js +34 -0
- package/dist/platform.d.ts +1 -1
- package/package.json +3 -2
package/dist/plan/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, } from './live.js';
|
|
2
2
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
3
|
-
export type { StepAction, PlannedStep, PlanDetail } from './diff.js';
|
|
3
|
+
export type { StepAction, PlannedStep, PlanDetail, OrgPostgresPlan } from './diff.js';
|
|
4
4
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
5
5
|
export type { StepSystem } from './step-system.js';
|
|
6
6
|
export { stepSystemOf } from './step-system.js';
|
|
@@ -21,13 +21,14 @@ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEME
|
|
|
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';
|
|
22
22
|
export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './platform-providers.js';
|
|
23
23
|
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
24
|
+
export { createZitadelLoginPolicy } from './zitadel-login-policy.js';
|
|
24
25
|
export { createZitadelProjectRoles } from './zitadel-project-roles.js';
|
|
25
26
|
export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './platform-sfu.js';
|
|
26
27
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
27
28
|
export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './mail-copy.js';
|
|
28
29
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
29
30
|
export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './providers-readers.js';
|
|
30
|
-
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
31
|
+
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeLoginHostResolver, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
31
32
|
export type { AgoraHostPort, AgoraHostValues } from './agora.js';
|
|
32
33
|
export type { BrokerPort } from './broker-entries.js';
|
|
33
34
|
export { PlatformBrokerPort } from './broker-entries.js';
|
|
@@ -57,3 +58,5 @@ export type { EdgePlatformTunnels, TunnelIngress, TunnelPort, TunnelSummary, } f
|
|
|
57
58
|
export type { ZitadelListAppsRawApp } from './executor.js';
|
|
58
59
|
export { adaptZitadelListApps } from './executor.js';
|
|
59
60
|
export { HttpJobsSchedulerClient } from './jobs-scheduler-http.js';
|
|
61
|
+
export type { OrgAppVersionRef, OrgAppVersionStatus, WtfalchPackageDrift, PlatformVersionStatus, ReadPlatformVersionStatusOptions, StatusCoolifyPort, StatusGithubPort, GhExec as StatusGhExec, } from './version-status.js';
|
|
62
|
+
export { readPlatformVersionStatus, wtfalchVersionsFromPnpmLockfile, providersStatusCoolifyPort, ghCliStatusGithubPort, } from './version-status.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -11,10 +11,11 @@ export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, Provide
|
|
|
11
11
|
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';
|
|
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
|
+
export { createZitadelLoginPolicy } from './zitadel-login-policy.js';
|
|
14
15
|
export { createZitadelProjectRoles } from './zitadel-project-roles.js';
|
|
15
16
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
16
17
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
17
|
-
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
18
|
+
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeLoginHostResolver, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
18
19
|
export { PlatformBrokerPort } from './broker-entries.js';
|
|
19
20
|
export { loginHostnameFor } from './login-host.js';
|
|
20
21
|
export { checkMailIdentityAudience } from './mail-identity.js';
|
|
@@ -29,3 +30,4 @@ export { createEdgeZones } from './edge-zones.js';
|
|
|
29
30
|
export { createEdgeTunnels } from './edge-tunnels.js';
|
|
30
31
|
export { adaptZitadelListApps } from './executor.js';
|
|
31
32
|
export { HttpJobsSchedulerClient } from './jobs-scheduler-http.js';
|
|
33
|
+
export { readPlatformVersionStatus, wtfalchVersionsFromPnpmLockfile, providersStatusCoolifyPort, ghCliStatusGithubPort, } from './version-status.js';
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1314,6 +1314,7 @@ const STEP_FIELDS = {
|
|
|
1314
1314
|
'coolify:chat': 'chat.usesDomain',
|
|
1315
1315
|
'coolify:calls': 'calls.enabled',
|
|
1316
1316
|
'coolify:calls-service': 'calls.enabled',
|
|
1317
|
+
'coolify:calls-chat': 'calls.enabled',
|
|
1317
1318
|
'ai:budget': 'ai',
|
|
1318
1319
|
'ai:key': 'ai',
|
|
1319
1320
|
'cms:site': 'site',
|
|
@@ -207,6 +207,15 @@ export declare const PLATFORM_AUTH_SMTP_SCOPE_KEY = "zitadel_platform_smtp_provi
|
|
|
207
207
|
* requested, so the invite goes out once. `none` when no owner is declared.
|
|
208
208
|
*/
|
|
209
209
|
export declare const PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY = "zitadel_platform_owner_invite_user_id";
|
|
210
|
+
/**
|
|
211
|
+
* #394: the id of the role-less ZITADEL machine user that reserves the
|
|
212
|
+
* username `admin` in the platform's own org, so nobody can sign up under a
|
|
213
|
+
* name Stalwart maps onto its Admin account. `foundry-admin` is reserved by
|
|
214
|
+
* the admin machine user itself.
|
|
215
|
+
*/
|
|
216
|
+
export declare const PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY = "zitadel_platform_reserved_admin_username";
|
|
217
|
+
/** #394: recorded once the platform's own ZITADEL org has self-registration forced off. */
|
|
218
|
+
export declare const PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY = "zitadel_platform_registration_closed";
|
|
210
219
|
export declare const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = "zitadel_platform_admin_user_id";
|
|
211
220
|
export declare const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = "zitadel_platform_admin_user_undelivered";
|
|
212
221
|
/** #283: the created instance's id, recorded once its owner token is delivered. */
|
|
@@ -234,6 +234,15 @@ export const PLATFORM_AUTH_SMTP_SCOPE_KEY = 'zitadel_platform_smtp_provider_id';
|
|
|
234
234
|
* requested, so the invite goes out once. `none` when no owner is declared.
|
|
235
235
|
*/
|
|
236
236
|
export const PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY = 'zitadel_platform_owner_invite_user_id';
|
|
237
|
+
/**
|
|
238
|
+
* #394: the id of the role-less ZITADEL machine user that reserves the
|
|
239
|
+
* username `admin` in the platform's own org, so nobody can sign up under a
|
|
240
|
+
* name Stalwart maps onto its Admin account. `foundry-admin` is reserved by
|
|
241
|
+
* the admin machine user itself.
|
|
242
|
+
*/
|
|
243
|
+
export const PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY = 'zitadel_platform_reserved_admin_username';
|
|
244
|
+
/** #394: recorded once the platform's own ZITADEL org has self-registration forced off. */
|
|
245
|
+
export const PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY = 'zitadel_platform_registration_closed';
|
|
237
246
|
export const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = 'zitadel_platform_admin_user_id';
|
|
238
247
|
export const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_admin_user_undelivered';
|
|
239
248
|
/** #283: the created instance's id, recorded once its owner token is delivered. */
|
|
@@ -1708,7 +1717,10 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
1708
1717
|
Boolean(recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY]) &&
|
|
1709
1718
|
// The ZITADEL run options reach the container only through the auth
|
|
1710
1719
|
// step's redeploy, so a platform recorded before them still plans it.
|
|
1711
|
-
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS
|
|
1720
|
+
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
1721
|
+
// #394: a platform applied before these existed plans the step again.
|
|
1722
|
+
Boolean(recordedIds[PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY]) &&
|
|
1723
|
+
Boolean(recordedIds[PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY])) {
|
|
1712
1724
|
return { step: null, blocked: null };
|
|
1713
1725
|
}
|
|
1714
1726
|
// The admin key was minted but never reached the host. ZITADEL shows a
|
|
@@ -2284,7 +2296,7 @@ function planAccessProvider(platform, recordedIds, executorBlocked) {
|
|
|
2284
2296
|
* auth/ZITADEL client: `hc` is an internal operational tool, not something
|
|
2285
2297
|
* an organisation member signs into. Whether this stays the platform's
|
|
2286
2298
|
* heartbeat server long-term, instead of or alongside the hand-set-up
|
|
2287
|
-
*
|
|
2299
|
+
* heartbeat instance, and what happens to that instance's own data,
|
|
2288
2300
|
* are both still open "Decide" items on issue #540's own thread -- not
|
|
2289
2301
|
* settled by this code.
|
|
2290
2302
|
*
|
|
@@ -2598,7 +2610,7 @@ function planAi(platform, recordedIds, executorBlocked) {
|
|
|
2598
2610
|
/**
|
|
2599
2611
|
* #175: Archon (https://github.com/wtfalch/archon), the one shared console,
|
|
2600
2612
|
* built fresh at `platform.archon.hostname` (`archon.<apex>`) rather than the
|
|
2601
|
-
* hand-deployed `operator
|
|
2613
|
+
* hand-deployed `operator` app it replaces (#165). One step,
|
|
2602
2614
|
* `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, covers the app, its
|
|
2603
2615
|
* database, its Cloudflare tunnel, Access app and DNS record, and its sign-in
|
|
2604
2616
|
* client in the platform's own ZITADEL -- the same bundling {@link planAuth}
|
|
@@ -86,6 +86,8 @@ export interface OrgPlatformReadiness {
|
|
|
86
86
|
callsReady: boolean;
|
|
87
87
|
/** The platform's own sign-in (#168), which every org's own ZITADEL org lives inside. */
|
|
88
88
|
authReady: boolean;
|
|
89
|
+
/** `https://<platform.auth.hostname>`, the platform's own sign-in issuer; `null` until {@link authReady}. The owner invite reads its issuer here, never from Agora's host config. */
|
|
90
|
+
authIssuer: string | null;
|
|
89
91
|
/** The Access provider id the platform recorded (`ACCESS_PROVIDER_RECORD_KEY`), which every gate requires. `null` or absent until the platform has recorded it. */
|
|
90
92
|
accessProviderId?: string | null;
|
|
91
93
|
/** The jobs scheduler (#173) is enabled, applied, has recorded its internal address, and its admin token is held. */
|
|
@@ -32,6 +32,7 @@ export const DEFAULT_ORG_PLATFORM_READINESS = {
|
|
|
32
32
|
aiReady: false,
|
|
33
33
|
callsReady: false,
|
|
34
34
|
authReady: false,
|
|
35
|
+
authIssuer: null,
|
|
35
36
|
jobsReady: false,
|
|
36
37
|
edgeReady: false,
|
|
37
38
|
missingKeys: { files: null, ai: null, jobs: null },
|
|
@@ -106,6 +107,7 @@ edgePortPresent = false) {
|
|
|
106
107
|
const jobsApplied = platform.jobs.enabled && Boolean(platformRecordedIds[`coolify:jobs:${JOBS_SCHEDULER_KEY}`]);
|
|
107
108
|
const jobsUrl = platformRecordedIds[JOBS_INTERNAL_URL_SCOPE_KEY];
|
|
108
109
|
const edgeApplied = platform.edge.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_EDGE_KEY);
|
|
110
|
+
const authReady = platform.auth.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_AUTH_KEY);
|
|
109
111
|
const missing = (applied, ref) => applied && heldRefs !== undefined && !heldRefs.has(ref) ? ref : null;
|
|
110
112
|
const missingKeys = {
|
|
111
113
|
files: missing(filesApplied, ORG_PLATFORM_KEY_REFS.files),
|
|
@@ -133,7 +135,8 @@ edgePortPresent = false) {
|
|
|
133
135
|
...(platformRecordedIds[ACCESS_PROVIDER_RECORD_KEY]
|
|
134
136
|
? { accessProviderId: platformRecordedIds[ACCESS_PROVIDER_RECORD_KEY] }
|
|
135
137
|
: {}),
|
|
136
|
-
authReady
|
|
138
|
+
authReady,
|
|
139
|
+
authIssuer: authReady ? `https://${platform.auth.hostname}` : null,
|
|
137
140
|
jobsReady: jobsApplied && jobsUrl !== undefined && missingKeys.jobs === null,
|
|
138
141
|
...(jobsApplied && jobsUrl === undefined ? { jobsUrlMissing: true } : {}),
|
|
139
142
|
edgeReady: edgeApplied && edgePortPresent,
|
|
@@ -90,7 +90,7 @@ export interface PlatformAuthDeps {
|
|
|
90
90
|
tunnels: PlatformTunnelsFactory;
|
|
91
91
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
92
92
|
/** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
|
|
93
|
-
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'inviteOrgOwner'> & PlatformZitadelSmtp;
|
|
93
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'inviteOrgOwner'> & PlatformZitadelSmtp;
|
|
94
94
|
/**
|
|
95
95
|
* Builds the ZITADEL port for the new instance from its issuer URL
|
|
96
96
|
* (`https://<auth.hostname>`). When set it is used instead of
|
|
@@ -9,7 +9,7 @@ import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrSta
|
|
|
9
9
|
import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
|
|
10
10
|
import { healthCheckSettings } from './health-check-settings.js';
|
|
11
11
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
12
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_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_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, 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_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, 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_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_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, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
12
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_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_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, 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_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_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_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, 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_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_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, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
13
13
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
14
14
|
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
15
15
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
@@ -500,10 +500,13 @@ export class FoundryPlatformStepExecutor {
|
|
|
500
500
|
if (platform.files.enabled && this.files && recordedIds[`coolify:app:${PLATFORM_FILES_KEY}`]) {
|
|
501
501
|
// A Coolify read error must not fail the whole plan: treat it as not drifted.
|
|
502
502
|
try {
|
|
503
|
-
|
|
503
|
+
const envDrifted = await filesRunnerEnvDrifted(this.files.coolify, recordedIds, {
|
|
504
504
|
publicHosts: this.files.publicHosts,
|
|
505
505
|
shareDomains: filesShareDomains(await this.declaredOrgStore.list()),
|
|
506
|
-
})
|
|
506
|
+
});
|
|
507
|
+
const bootstrapOperatorDrifted = !envDrifted &&
|
|
508
|
+
(await filesBootstrapOperatorDrifted(this.files.coolify, recordedIds[`coolify:app:${PLATFORM_FILES_KEY}`], recordedIds));
|
|
509
|
+
if (envDrifted || bootstrapOperatorDrifted)
|
|
507
510
|
result.files = true;
|
|
508
511
|
}
|
|
509
512
|
catch {
|
|
@@ -903,6 +906,8 @@ const PLATFORM_AUTH_TUNNEL_SCOPE_KEY = 'cloudflare_platform_auth_tunnel';
|
|
|
903
906
|
/** Recorded once the ZITADEL app has been deployed, so ZITADEL may have written to its database. */
|
|
904
907
|
const PLATFORM_AUTH_ZITADEL_DEPLOYED_SCOPE_KEY = 'zitadel_platform_auth_deployed';
|
|
905
908
|
const PLATFORM_AUTH_ADMIN_USER_NAME = 'foundry-admin';
|
|
909
|
+
/** #394: reserved so nobody signs up as the name Stalwart maps onto its Admin account. */
|
|
910
|
+
const PLATFORM_AUTH_RESERVED_USERNAME = 'admin';
|
|
906
911
|
/** #216 (building on #209/#215): the narrower `IAM_LOGIN_CLIENT` machine user for the login app and the broker. */
|
|
907
912
|
const PLATFORM_AUTH_LOGIN_CLIENT_USER_NAME = 'foundry-login-client';
|
|
908
913
|
const PLATFORM_AUTH_ORG_NAME = 'foundry';
|
|
@@ -1125,7 +1130,7 @@ async function liveLoginClientTokenHash(coolify, appId) {
|
|
|
1125
1130
|
* `platform.auth.hostname`, then (only the first time) foundry's own admin
|
|
1126
1131
|
* machine user, handed to {@link PlatformAuthDeps.deliverSecret}. Env values
|
|
1127
1132
|
* and instance settings are `wtfalch/auth`'s own `docker-compose.yml`
|
|
1128
|
-
* (checked 2026-09-29), with `ZITADEL_DOMAIN`/`
|
|
1133
|
+
* (checked 2026-09-29), with `ZITADEL_DOMAIN`/`companyincluded.com` replaced by this
|
|
1129
1134
|
* platform's own `apex`/`auth.hostname` wherever the compose named one --
|
|
1130
1135
|
* every other setting it leaves open (login v2 required, external port 443,
|
|
1131
1136
|
* TLS terminated at Cloudflare, `PASSWORDCHANGEREQUIRED` false) is kept
|
|
@@ -1815,6 +1820,28 @@ async function provisionPlatformAuth(context, deps, owners = []) {
|
|
|
1815
1820
|
}
|
|
1816
1821
|
}
|
|
1817
1822
|
}
|
|
1823
|
+
// ---- reserved username and closed registration, once each (#394) ----------
|
|
1824
|
+
// Outside the blocks above so a platform applied before this existed gets
|
|
1825
|
+
// it on its next apply; `planAuth` plans the step until both keys exist.
|
|
1826
|
+
// Last, so a failure here holds up neither the SMTP provider nor the invite.
|
|
1827
|
+
const policyRecorded = context.recordedIds();
|
|
1828
|
+
if (!policyRecorded[PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY] ||
|
|
1829
|
+
!policyRecorded[PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY]) {
|
|
1830
|
+
const policyZitadel = deps.zitadelForInstance?.(`https://${hostname}`) ?? deps.zitadel;
|
|
1831
|
+
let policyOrgId = policyRecorded[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
|
|
1832
|
+
if (!policyOrgId) {
|
|
1833
|
+
policyOrgId = (await policyZitadel.findOrCreateOrg(PLATFORM_AUTH_ORG_NAME)).org.id;
|
|
1834
|
+
await record(PLATFORM_ZITADEL_ORG_SCOPE_KEY, policyOrgId);
|
|
1835
|
+
}
|
|
1836
|
+
if (!policyRecorded[PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY]) {
|
|
1837
|
+
const { userId } = await policyZitadel.reserveUserName(policyOrgId, PLATFORM_AUTH_RESERVED_USERNAME);
|
|
1838
|
+
await record(PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, userId);
|
|
1839
|
+
}
|
|
1840
|
+
if (!policyRecorded[PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY]) {
|
|
1841
|
+
await policyZitadel.closeSelfRegistration(policyOrgId);
|
|
1842
|
+
await record(PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, 'closed');
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1818
1845
|
return { resourceId: zitadelAppId };
|
|
1819
1846
|
}
|
|
1820
1847
|
/** Lettermint's SMTP relay, as ZITADEL's SMTP provider wants it: `host:port`, STARTTLS. */
|
|
@@ -2034,7 +2061,7 @@ const PLATFORM_MAIL_ZITADEL_PROJECT_NAME = 'mail';
|
|
|
2034
2061
|
*
|
|
2035
2062
|
* **Why not `@wtfalch/email/postmaster`'s `mailObjects`/`applyObjects`**, the
|
|
2036
2063
|
* instance-bootstrap helper `wtfalch-manage` already uses for the hand-built
|
|
2037
|
-
* `mail.
|
|
2064
|
+
* `mail.companyincluded.com`: that plan also wires an `AcmeProvider` and `DnsServer`
|
|
2038
2065
|
* for Stalwart's own DNS-01 ACME challenge, and an `MtaRoute` that relays
|
|
2039
2066
|
* every outbound message through the earlier mail provider -- the estate moved transactional
|
|
2040
2067
|
* mail off it onto Lettermint before this issue (`@wtfalch/mailer` ADR
|
|
@@ -2056,7 +2083,7 @@ const PLATFORM_MAIL_ZITADEL_PROJECT_NAME = 'mail';
|
|
|
2056
2083
|
* `StepExecutionContext` to carry the platform, which `plan-apply.ts`'s
|
|
2057
2084
|
* own doc comment already scoped out of #167 as more than a small change
|
|
2058
2085
|
* -- a follow-up issue, not this one. (#205 removed the org step
|
|
2059
|
-
* `coolify:mail`'s own fallback to the hand-built
|
|
2086
|
+
* `coolify:mail`'s own fallback to the hand-built mail
|
|
2060
2087
|
* instance -- `diff.ts`'s `planMail` already blocks the step until the
|
|
2061
2088
|
* platform's own mail is applied, so the fallback only ever served a
|
|
2062
2089
|
* plan recorded before that, and now fails loudly instead.)
|
|
@@ -2809,7 +2836,7 @@ async function switchOnArchonIdTokenUserinfo(context, deps) {
|
|
|
2809
2836
|
* - No API updates an app's Access policy, so a changed include list closes
|
|
2810
2837
|
* the tunnel hostname, deletes the Access app and builds it again.
|
|
2811
2838
|
* - #197: a `cloudflared` connector runs for this tunnel, as in `provisionPlatformAuth`.
|
|
2812
|
-
* - Retiring `operator
|
|
2839
|
+
* - Retiring the hand-deployed `operator` app stays a human step.
|
|
2813
2840
|
* - #475: deploys this app with a stable `custom_network_aliases` instead of a
|
|
2814
2841
|
* fixed `custom_internal_name`, so Coolify runs its rolling update (start
|
|
2815
2842
|
* the new container, wait for health, then stop the old one) instead of
|
|
@@ -3096,10 +3123,23 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3096
3123
|
if (ownerInviteUserId && ownerInviteUserId !== 'none') {
|
|
3097
3124
|
envs.push({ key: 'AUTHZ_BOOTSTRAP_OPERATOR', value: ownerInviteUserId });
|
|
3098
3125
|
}
|
|
3126
|
+
// #501: a FILES_URL or FILES_AUDIENCE already on the app is kept (the #265
|
|
3127
|
+
// rule above), and hashed as the live value so it never reads as a changed
|
|
3128
|
+
// env.
|
|
3129
|
+
const keepLiveFilesEnv = new Set();
|
|
3130
|
+
for (const key of ['FILES_URL', 'FILES_AUDIENCE']) {
|
|
3131
|
+
const live = await currentEnvValue(deps.coolify, appId, key);
|
|
3132
|
+
if (live) {
|
|
3133
|
+
keepLiveFilesEnv.add(key);
|
|
3134
|
+
archonEnvHashOverrides.set(key, live);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3099
3137
|
// A value with a line break (the PEM management keys) must be flagged:
|
|
3100
3138
|
// unflagged, Coolify writes it into `.env` unquoted and the container never
|
|
3101
3139
|
// starts.
|
|
3102
|
-
envs.push(...inputs.envs
|
|
3140
|
+
envs.push(...inputs.envs
|
|
3141
|
+
.filter((e) => !keepLiveFilesEnv.has(e.key))
|
|
3142
|
+
.map((e) => (e.value.includes('\n') ? { ...e, multiline: true } : e)));
|
|
3103
3143
|
// #436: /backups needs a Coolify token scoped to read only, never the
|
|
3104
3144
|
// root token `putArchonMasterKeys` above already held. Nothing mints that
|
|
3105
3145
|
// token over Coolify's API (this ref's own doc comment), so until a human
|
|
@@ -3196,7 +3236,7 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3196
3236
|
clientIds: [clientId],
|
|
3197
3237
|
origins: [`https://${hostname}`],
|
|
3198
3238
|
// #211: `wtfalch/auth`'s broker (`broker/src/apps.ts`'s `parseAppFields`)
|
|
3199
|
-
// falls back to
|
|
3239
|
+
// falls back to its own default sender for an entry with no
|
|
3200
3240
|
// `from`, and (`wtfalch/auth#179`) an entry with no `from` is dropped
|
|
3201
3241
|
// entirely -- either way, unset here means Archon's mail comes from
|
|
3202
3242
|
// the wrong platform or not at all.
|
|
@@ -3900,6 +3940,23 @@ async function archonBootstrapOperatorDrifted(coolify, appId, recordedIds) {
|
|
|
3900
3940
|
return false;
|
|
3901
3941
|
}
|
|
3902
3942
|
}
|
|
3943
|
+
/**
|
|
3944
|
+
* #501: {@link archonBootstrapOperatorDrifted} for the files runner. The owner
|
|
3945
|
+
* invite can record after the files step's app id does, so an owner invited
|
|
3946
|
+
* later still gets `AUTHZ_BOOTSTRAP_OPERATOR`. A failed read is not drift.
|
|
3947
|
+
*/
|
|
3948
|
+
async function filesBootstrapOperatorDrifted(coolify, appId, recordedIds) {
|
|
3949
|
+
const invitedUserId = recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
|
|
3950
|
+
if (!appId || !invitedUserId || invitedUserId === 'none')
|
|
3951
|
+
return false;
|
|
3952
|
+
try {
|
|
3953
|
+
const live = await currentEnvValue(coolify, appId, 'AUTHZ_BOOTSTRAP_OPERATOR');
|
|
3954
|
+
return live !== invitedUserId;
|
|
3955
|
+
}
|
|
3956
|
+
catch {
|
|
3957
|
+
return false;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3903
3960
|
/**
|
|
3904
3961
|
* #480: whether the live app disagrees with the patch
|
|
3905
3962
|
* `provisionPlatformArchon` sends on every run -- `is_consistent_container_name_enabled`
|
|
@@ -4021,15 +4078,10 @@ async function collectArchonServiceInputs(platform, deps, recorded) {
|
|
|
4021
4078
|
for (const service of services) {
|
|
4022
4079
|
if (service === 'files') {
|
|
4023
4080
|
envs.push(
|
|
4024
|
-
// #
|
|
4025
|
-
// internal
|
|
4026
|
-
//
|
|
4027
|
-
|
|
4028
|
-
// `src/lib/files-admin/client.ts`), and that origin is also the
|
|
4029
|
-
// signing audience, so it must equal Files' own `APP_URL` exactly
|
|
4030
|
-
// (this file, the files runner env above: `https://${platform.
|
|
4031
|
-
// files.hostname}`).
|
|
4032
|
-
{ key: 'FILES_URL', value: `https://${platform.files.hostname}` },
|
|
4081
|
+
// #501: Files is private and has no DNS, so FILES_URL is the recorded
|
|
4082
|
+
// internal address; FILES_AUDIENCE is the exact HTTPS origin the
|
|
4083
|
+
// signature is made for, equal to Files' own `APP_URL`.
|
|
4084
|
+
{ key: 'FILES_URL', value: recorded[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY] }, { key: 'FILES_AUDIENCE', value: `https://${platform.files.hostname}` },
|
|
4033
4085
|
// #436: Company Included's own Files organisation, provisioned by
|
|
4034
4086
|
// the ordinary per-org `planFilesOrganisation` step under its own
|
|
4035
4087
|
// org id -- `executor.ts`'s `filesOrganisation` calls
|
|
@@ -5357,10 +5409,10 @@ const PLATFORM_FILES_RUNNER_SCOPE_KEY = 'coolify_platform_files_runner_app';
|
|
|
5357
5409
|
* and the optional `KEYS_AUDIT_TOKEN`), `R2_BUCKET`/`R2_ENDPOINT` from
|
|
5358
5410
|
* {@link PlatformFilesDeps.r2} (#380, also required), the optional
|
|
5359
5411
|
* `FILES_CLAMD_HOST`/`FILES_CLAMD_PORT`, `FILES_PUBLIC_HOSTS` and
|
|
5360
|
-
* `JOBS_SIGNING_SECRETS` the host may supply (#380),
|
|
5361
|
-
* {@link PLATFORM_FILES_RUNNER_DEFAULTS} (#380, written only when absent)
|
|
5362
|
-
* `AUTHZ_BOOTSTRAP_OPERATOR
|
|
5363
|
-
* still set by a person
|
|
5412
|
+
* `JOBS_SIGNING_SECRETS` the host may supply (#380), the three
|
|
5413
|
+
* {@link PLATFORM_FILES_RUNNER_DEFAULTS} (#380, written only when absent),
|
|
5414
|
+
* and (#501) `AUTHZ_BOOTSTRAP_OPERATOR`, the same person `platform:archon`
|
|
5415
|
+
* names. The rest of files' `.env.example` is still set by a person.
|
|
5364
5416
|
* - The files project lives in the platform's `foundry` ZITADEL org. An
|
|
5365
5417
|
* org's Agora requests it as an audience scope from its own ZITADEL org;
|
|
5366
5418
|
* whether that needs a project grant per org is not decided here.
|
|
@@ -5539,6 +5591,11 @@ shareDomains) {
|
|
|
5539
5591
|
key: 'JOBS_SIGNING_SECRETS',
|
|
5540
5592
|
value: deps.jobsSigningSecrets,
|
|
5541
5593
|
});
|
|
5594
|
+
// #501: the same person platform:archon names; Files has no sign-in page.
|
|
5595
|
+
const ownerInviteUserId = recorded[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
|
|
5596
|
+
if (ownerInviteUserId && ownerInviteUserId !== 'none') {
|
|
5597
|
+
runnerOptionalEnvs.push({ key: 'AUTHZ_BOOTSTRAP_OPERATOR', value: ownerInviteUserId });
|
|
5598
|
+
}
|
|
5542
5599
|
const runnerEnvs = [
|
|
5543
5600
|
{ key: 'DATABASE_URL', value: runtimeUrl },
|
|
5544
5601
|
// The management verifier's audience, which must be an HTTPS origin. The
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CloudflareClient } from '@wtfalch/providers/cloudflare';
|
|
2
2
|
import type { CoolifyClient } from '@wtfalch/providers/coolify';
|
|
3
3
|
import { type ZitadelClient } from '@wtfalch/providers/zitadel';
|
|
4
|
-
import type
|
|
4
|
+
import { type OrgId } from '../org.js';
|
|
5
5
|
import type { DeclaredOrgStore } from '../store.js';
|
|
6
6
|
import type { AiReader, CloudflareReader, CmsReader, CoolifyReader, LiveResource, ZitadelReader } from './live.js';
|
|
7
7
|
/** The `CoolifyClient` methods the readers call, so a host's own implementation needs no cast. */
|
|
@@ -68,6 +68,13 @@ export type ScopeResolver = (orgId: OrgId) => Promise<string | null>;
|
|
|
68
68
|
* which `computePlanDetail` (`diff.ts`) already reads as "no live resource".
|
|
69
69
|
*/
|
|
70
70
|
export declare function storeScopeResolver(store: DeclaredOrgStore, serviceKey: string): ScopeResolver;
|
|
71
|
+
/**
|
|
72
|
+
* #517: resolves the login host an org's sign-in is recorded on
|
|
73
|
+
* (`zitadel:login-host:<orgId>`, written by `zitadelLoginHost`), for
|
|
74
|
+
* {@link ProvidersZitadelReader}. The key holds the org id, so it cannot be a
|
|
75
|
+
* fixed {@link storeScopeResolver} `serviceKey`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function storeLoginHostResolver(store: DeclaredOrgStore): ScopeResolver;
|
|
71
78
|
/**
|
|
72
79
|
* Resolves an estate {@link OrgId} to the provider ids this org's applied
|
|
73
80
|
* state has recorded for one reader's `system`, inverted so a reader can
|
|
@@ -131,12 +138,18 @@ export declare class ProvidersCloudflareReader implements CloudflareReader {
|
|
|
131
138
|
* resource per project and per project grant in a ZITADEL org (v1 scope —
|
|
132
139
|
* see that method's own doc comment), plus (#510) a synthesized `issuer`
|
|
133
140
|
* resource once the org's own ZITADEL org is confirmed live.
|
|
141
|
+
*
|
|
142
|
+
* `resolveLoginHost` (#517) is optional; without it the issuer carries no
|
|
143
|
+
* host. A recorded host under `auth.` is the org's own domain, so the issuer
|
|
144
|
+
* then also says `usesDomain: true`; any other host (tier 1, shared) says
|
|
145
|
+
* nothing about `usesDomain`, which `planIssuer` reads as no move.
|
|
134
146
|
*/
|
|
135
147
|
export declare class ProvidersZitadelReader implements ZitadelReader {
|
|
136
148
|
private readonly client;
|
|
137
149
|
private readonly resolveScope;
|
|
138
150
|
private readonly resolveRecordedIds;
|
|
139
|
-
|
|
151
|
+
private readonly resolveLoginHost;
|
|
152
|
+
constructor(client: ZitadelClient, resolveScope: ScopeResolver, resolveRecordedIds: RecordedIds, resolveLoginHost?: ScopeResolver);
|
|
140
153
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
141
154
|
/**
|
|
142
155
|
* #510: `client.listResources` reports only what is *inside* a ZITADEL org
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ZitadelError } from '@wtfalch/providers/zitadel';
|
|
2
|
-
import {
|
|
2
|
+
import { APP_ENTRY_KEY } from '../org.js';
|
|
3
|
+
import { BOULE_MAIL_TOKEN_ENV, BOULE_MANAGED_ENV_KEYS } from './boule.js';
|
|
3
4
|
/**
|
|
4
5
|
* A {@link ScopeResolver} backed by `AppliedState.serviceIds` (`../state.ts`)
|
|
5
6
|
* — the one place foundry records a provider's own id for an org today,
|
|
@@ -30,6 +31,18 @@ export function storeScopeResolver(store, serviceKey) {
|
|
|
30
31
|
return applied?.serviceIds[serviceKey] || null;
|
|
31
32
|
};
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* #517: resolves the login host an org's sign-in is recorded on
|
|
36
|
+
* (`zitadel:login-host:<orgId>`, written by `zitadelLoginHost`), for
|
|
37
|
+
* {@link ProvidersZitadelReader}. The key holds the org id, so it cannot be a
|
|
38
|
+
* fixed {@link storeScopeResolver} `serviceKey`.
|
|
39
|
+
*/
|
|
40
|
+
export function storeLoginHostResolver(store) {
|
|
41
|
+
return async (orgId) => {
|
|
42
|
+
const applied = await store.getAppliedState(orgId);
|
|
43
|
+
return applied?.serviceIds[`zitadel:login-host:${orgId}`] || null;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
33
46
|
export function storeRecordedIds(store, system) {
|
|
34
47
|
return async (orgId) => {
|
|
35
48
|
const applied = await store.getAppliedState(orgId);
|
|
@@ -247,7 +260,7 @@ async function withAgoraEnvConfig(client, resource, recorded) {
|
|
|
247
260
|
* `executor.ts` would write. Gated and failing exactly like
|
|
248
261
|
* {@link withAgoraEnvConfig}.
|
|
249
262
|
*/
|
|
250
|
-
async function withBouleEnvConfig(client, resource, recorded) {
|
|
263
|
+
async function withBouleEnvConfig(client, resource, recorded, liveKeys) {
|
|
251
264
|
if (resource.kind !== 'application' || !recorded[resource.key]?.startsWith('boule:')) {
|
|
252
265
|
return resource;
|
|
253
266
|
}
|
|
@@ -257,7 +270,20 @@ async function withBouleEnvConfig(client, resource, recorded) {
|
|
|
257
270
|
if (!e.is_preview && managed.includes(e.key))
|
|
258
271
|
env[e.key] = e.value;
|
|
259
272
|
}
|
|
260
|
-
|
|
273
|
+
// The hosted app's Lettermint token, as `config.appMailToken`: the one
|
|
274
|
+
// Boule must hold too. In memory only, compared by `planBoule` and never
|
|
275
|
+
// written to a step, description or log.
|
|
276
|
+
const orgId = recorded[resource.key]?.slice('boule:'.length);
|
|
277
|
+
const appUuid = Object.entries(recorded).find(([, value]) => value === `app:${orgId}:${APP_ENTRY_KEY}`)?.[0];
|
|
278
|
+
// A recorded app that is not in the live listing is gone: no app token, and
|
|
279
|
+
// no read of its envs. A listed app whose read fails fails like any read.
|
|
280
|
+
const appToken = appUuid && liveKeys.has(appUuid)
|
|
281
|
+
? (await client.listEnvs(appUuid)).find((e) => !e.is_preview && e.key === BOULE_MAIL_TOKEN_ENV)?.value
|
|
282
|
+
: undefined;
|
|
283
|
+
return {
|
|
284
|
+
...resource,
|
|
285
|
+
config: { ...resource.config, env, ...(appToken ? { appMailToken: appToken } : {}) },
|
|
286
|
+
};
|
|
261
287
|
}
|
|
262
288
|
/** `@wtfalch/providers`' own `LiveResource` (`resource.ts`) has no `system` field — foundry's adds it at this boundary. */
|
|
263
289
|
function toFoundryResources(system, resources) {
|
|
@@ -290,7 +316,8 @@ export class ProvidersCoolifyReader {
|
|
|
290
316
|
const withEnvNames = await Promise.all(withHealthCheck.map((r) => withEnvNamesConfig(this.client, r, recorded)));
|
|
291
317
|
const withDbUrl = await Promise.all(withEnvNames.map((r) => withDatabaseUrlConfig(this.client, r, recorded)));
|
|
292
318
|
const withAgoraConfig = await Promise.all(withDbUrl.map((r) => withAgoraEnvConfig(this.client, r, recorded)));
|
|
293
|
-
const
|
|
319
|
+
const liveKeys = new Set(withAgoraConfig.map((r) => r.key));
|
|
320
|
+
const withBouleConfig = await Promise.all(withAgoraConfig.map((r) => withBouleEnvConfig(this.client, r, recorded, liveKeys)));
|
|
294
321
|
return applyRecordedIds(withBouleConfig, recorded);
|
|
295
322
|
}
|
|
296
323
|
/** The project's resources as Coolify reports them, unmapped: what adopt checks against a declaration. */
|
|
@@ -350,15 +377,22 @@ export class ProvidersCloudflareReader {
|
|
|
350
377
|
* resource per project and per project grant in a ZITADEL org (v1 scope —
|
|
351
378
|
* see that method's own doc comment), plus (#510) a synthesized `issuer`
|
|
352
379
|
* resource once the org's own ZITADEL org is confirmed live.
|
|
380
|
+
*
|
|
381
|
+
* `resolveLoginHost` (#517) is optional; without it the issuer carries no
|
|
382
|
+
* host. A recorded host under `auth.` is the org's own domain, so the issuer
|
|
383
|
+
* then also says `usesDomain: true`; any other host (tier 1, shared) says
|
|
384
|
+
* nothing about `usesDomain`, which `planIssuer` reads as no move.
|
|
353
385
|
*/
|
|
354
386
|
export class ProvidersZitadelReader {
|
|
355
387
|
client;
|
|
356
388
|
resolveScope;
|
|
357
389
|
resolveRecordedIds;
|
|
358
|
-
|
|
390
|
+
resolveLoginHost;
|
|
391
|
+
constructor(client, resolveScope, resolveRecordedIds, resolveLoginHost = async () => null) {
|
|
359
392
|
this.client = client;
|
|
360
393
|
this.resolveScope = resolveScope;
|
|
361
394
|
this.resolveRecordedIds = resolveRecordedIds;
|
|
395
|
+
this.resolveLoginHost = resolveLoginHost;
|
|
362
396
|
}
|
|
363
397
|
async listResources(orgId) {
|
|
364
398
|
const zitadelOrgId = await this.resolveScope(orgId);
|
|
@@ -402,7 +436,11 @@ export class ProvidersZitadelReader {
|
|
|
402
436
|
return null;
|
|
403
437
|
throw err;
|
|
404
438
|
}
|
|
405
|
-
|
|
439
|
+
const host = await this.resolveLoginHost(orgId);
|
|
440
|
+
if (!host)
|
|
441
|
+
return { system: 'zitadel', kind: 'issuer', key: orgId, config: {} };
|
|
442
|
+
const config = host.startsWith('auth.') ? { usesDomain: true, host } : { host };
|
|
443
|
+
return { system: 'zitadel', kind: 'issuer', key: orgId, config };
|
|
406
444
|
}
|
|
407
445
|
}
|
|
408
446
|
/**
|