@wtfalch/foundry 0.29.0 → 0.31.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/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 +162 -10
- package/dist/plan/executor.d.ts +93 -0
- package/dist/plan/executor.js +345 -9
- 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 +18 -0
- package/dist/plan/platform-diff.js +35 -5
- 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 +85 -23
- package/dist/plan/providers-readers.d.ts +14 -1
- package/dist/plan/providers-readers.js +25 -2
- 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 +4 -3
|
@@ -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_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, 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.)
|
|
@@ -2299,7 +2326,10 @@ async function provisionPlatformMail(context, deps) {
|
|
|
2299
2326
|
const { project } = await zitadel.findOrCreateProject(org.id, PLATFORM_MAIL_ZITADEL_PROJECT_NAME);
|
|
2300
2327
|
const recordedProjectId = recorded[PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY];
|
|
2301
2328
|
const adminKeyRecorded = Boolean(recorded[PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY]);
|
|
2302
|
-
|
|
2329
|
+
// #661: a platform applied before the local-delivery route existed gets it
|
|
2330
|
+
// here, over the same recovery-admin credential, and mints nothing.
|
|
2331
|
+
const routeRecorded = recorded[PLATFORM_MAIL_ROUTE_SCOPE_KEY] === PLATFORM_MAIL_ROUTE_WANTED;
|
|
2332
|
+
if (adminKeyRecorded && (recordedProjectId !== project.id || !routeRecorded)) {
|
|
2303
2333
|
// #346 round 3: the OIDC-directory write below (next block) runs only
|
|
2304
2334
|
// once, while the admin key is unrecorded -- so once it is recorded, a
|
|
2305
2335
|
// changed project id (the project was deleted and recreated, ZITADEL was
|
|
@@ -2339,6 +2369,7 @@ async function provisionPlatformMail(context, deps) {
|
|
|
2339
2369
|
},
|
|
2340
2370
|
});
|
|
2341
2371
|
await record(PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, project.id);
|
|
2372
|
+
await record(PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED);
|
|
2342
2373
|
}
|
|
2343
2374
|
catch (error) {
|
|
2344
2375
|
throw redactedMailError(error, [password]);
|
|
@@ -2378,6 +2409,7 @@ async function provisionPlatformMail(context, deps) {
|
|
|
2378
2409
|
oidc: { issuerUrl: issuer, audience: project.id },
|
|
2379
2410
|
},
|
|
2380
2411
|
});
|
|
2412
|
+
await record(PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED);
|
|
2381
2413
|
// Recorded and persisted before the secret leaves this function: if
|
|
2382
2414
|
// delivery fails for good, the next plan reports the lost key rather
|
|
2383
2415
|
// than skipping or minting again.
|
|
@@ -2809,7 +2841,7 @@ async function switchOnArchonIdTokenUserinfo(context, deps) {
|
|
|
2809
2841
|
* - No API updates an app's Access policy, so a changed include list closes
|
|
2810
2842
|
* the tunnel hostname, deletes the Access app and builds it again.
|
|
2811
2843
|
* - #197: a `cloudflared` connector runs for this tunnel, as in `provisionPlatformAuth`.
|
|
2812
|
-
* - Retiring `operator
|
|
2844
|
+
* - Retiring the hand-deployed `operator` app stays a human step.
|
|
2813
2845
|
* - #475: deploys this app with a stable `custom_network_aliases` instead of a
|
|
2814
2846
|
* fixed `custom_internal_name`, so Coolify runs its rolling update (start
|
|
2815
2847
|
* the new container, wait for health, then stop the old one) instead of
|
|
@@ -3096,10 +3128,23 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3096
3128
|
if (ownerInviteUserId && ownerInviteUserId !== 'none') {
|
|
3097
3129
|
envs.push({ key: 'AUTHZ_BOOTSTRAP_OPERATOR', value: ownerInviteUserId });
|
|
3098
3130
|
}
|
|
3131
|
+
// #501: a FILES_URL or FILES_AUDIENCE already on the app is kept (the #265
|
|
3132
|
+
// rule above), and hashed as the live value so it never reads as a changed
|
|
3133
|
+
// env.
|
|
3134
|
+
const keepLiveFilesEnv = new Set();
|
|
3135
|
+
for (const key of ['FILES_URL', 'FILES_AUDIENCE']) {
|
|
3136
|
+
const live = await currentEnvValue(deps.coolify, appId, key);
|
|
3137
|
+
if (live) {
|
|
3138
|
+
keepLiveFilesEnv.add(key);
|
|
3139
|
+
archonEnvHashOverrides.set(key, live);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3099
3142
|
// A value with a line break (the PEM management keys) must be flagged:
|
|
3100
3143
|
// unflagged, Coolify writes it into `.env` unquoted and the container never
|
|
3101
3144
|
// starts.
|
|
3102
|
-
envs.push(...inputs.envs
|
|
3145
|
+
envs.push(...inputs.envs
|
|
3146
|
+
.filter((e) => !keepLiveFilesEnv.has(e.key))
|
|
3147
|
+
.map((e) => (e.value.includes('\n') ? { ...e, multiline: true } : e)));
|
|
3103
3148
|
// #436: /backups needs a Coolify token scoped to read only, never the
|
|
3104
3149
|
// root token `putArchonMasterKeys` above already held. Nothing mints that
|
|
3105
3150
|
// token over Coolify's API (this ref's own doc comment), so until a human
|
|
@@ -3196,7 +3241,7 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3196
3241
|
clientIds: [clientId],
|
|
3197
3242
|
origins: [`https://${hostname}`],
|
|
3198
3243
|
// #211: `wtfalch/auth`'s broker (`broker/src/apps.ts`'s `parseAppFields`)
|
|
3199
|
-
// falls back to
|
|
3244
|
+
// falls back to its own default sender for an entry with no
|
|
3200
3245
|
// `from`, and (`wtfalch/auth#179`) an entry with no `from` is dropped
|
|
3201
3246
|
// entirely -- either way, unset here means Archon's mail comes from
|
|
3202
3247
|
// the wrong platform or not at all.
|
|
@@ -3900,6 +3945,23 @@ async function archonBootstrapOperatorDrifted(coolify, appId, recordedIds) {
|
|
|
3900
3945
|
return false;
|
|
3901
3946
|
}
|
|
3902
3947
|
}
|
|
3948
|
+
/**
|
|
3949
|
+
* #501: {@link archonBootstrapOperatorDrifted} for the files runner. The owner
|
|
3950
|
+
* invite can record after the files step's app id does, so an owner invited
|
|
3951
|
+
* later still gets `AUTHZ_BOOTSTRAP_OPERATOR`. A failed read is not drift.
|
|
3952
|
+
*/
|
|
3953
|
+
async function filesBootstrapOperatorDrifted(coolify, appId, recordedIds) {
|
|
3954
|
+
const invitedUserId = recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
|
|
3955
|
+
if (!appId || !invitedUserId || invitedUserId === 'none')
|
|
3956
|
+
return false;
|
|
3957
|
+
try {
|
|
3958
|
+
const live = await currentEnvValue(coolify, appId, 'AUTHZ_BOOTSTRAP_OPERATOR');
|
|
3959
|
+
return live !== invitedUserId;
|
|
3960
|
+
}
|
|
3961
|
+
catch {
|
|
3962
|
+
return false;
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3903
3965
|
/**
|
|
3904
3966
|
* #480: whether the live app disagrees with the patch
|
|
3905
3967
|
* `provisionPlatformArchon` sends on every run -- `is_consistent_container_name_enabled`
|
|
@@ -4021,15 +4083,10 @@ async function collectArchonServiceInputs(platform, deps, recorded) {
|
|
|
4021
4083
|
for (const service of services) {
|
|
4022
4084
|
if (service === 'files') {
|
|
4023
4085
|
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}` },
|
|
4086
|
+
// #501: Files is private and has no DNS, so FILES_URL is the recorded
|
|
4087
|
+
// internal address; FILES_AUDIENCE is the exact HTTPS origin the
|
|
4088
|
+
// signature is made for, equal to Files' own `APP_URL`.
|
|
4089
|
+
{ key: 'FILES_URL', value: recorded[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY] }, { key: 'FILES_AUDIENCE', value: `https://${platform.files.hostname}` },
|
|
4033
4090
|
// #436: Company Included's own Files organisation, provisioned by
|
|
4034
4091
|
// the ordinary per-org `planFilesOrganisation` step under its own
|
|
4035
4092
|
// org id -- `executor.ts`'s `filesOrganisation` calls
|
|
@@ -5357,10 +5414,10 @@ const PLATFORM_FILES_RUNNER_SCOPE_KEY = 'coolify_platform_files_runner_app';
|
|
|
5357
5414
|
* and the optional `KEYS_AUDIT_TOKEN`), `R2_BUCKET`/`R2_ENDPOINT` from
|
|
5358
5415
|
* {@link PlatformFilesDeps.r2} (#380, also required), the optional
|
|
5359
5416
|
* `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
|
|
5417
|
+
* `JOBS_SIGNING_SECRETS` the host may supply (#380), the three
|
|
5418
|
+
* {@link PLATFORM_FILES_RUNNER_DEFAULTS} (#380, written only when absent),
|
|
5419
|
+
* and (#501) `AUTHZ_BOOTSTRAP_OPERATOR`, the same person `platform:archon`
|
|
5420
|
+
* names. The rest of files' `.env.example` is still set by a person.
|
|
5364
5421
|
* - The files project lives in the platform's `foundry` ZITADEL org. An
|
|
5365
5422
|
* org's Agora requests it as an audience scope from its own ZITADEL org;
|
|
5366
5423
|
* whether that needs a project grant per org is not decided here.
|
|
@@ -5539,6 +5596,11 @@ shareDomains) {
|
|
|
5539
5596
|
key: 'JOBS_SIGNING_SECRETS',
|
|
5540
5597
|
value: deps.jobsSigningSecrets,
|
|
5541
5598
|
});
|
|
5599
|
+
// #501: the same person platform:archon names; Files has no sign-in page.
|
|
5600
|
+
const ownerInviteUserId = recorded[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
|
|
5601
|
+
if (ownerInviteUserId && ownerInviteUserId !== 'none') {
|
|
5602
|
+
runnerOptionalEnvs.push({ key: 'AUTHZ_BOOTSTRAP_OPERATOR', value: ownerInviteUserId });
|
|
5603
|
+
}
|
|
5542
5604
|
const runnerEnvs = [
|
|
5543
5605
|
{ key: 'DATABASE_URL', value: runtimeUrl },
|
|
5544
5606
|
// The management verifier's audience, which must be an HTTPS origin. The
|
|
@@ -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
|
|
@@ -31,6 +31,18 @@ export function storeScopeResolver(store, serviceKey) {
|
|
|
31
31
|
return applied?.serviceIds[serviceKey] || null;
|
|
32
32
|
};
|
|
33
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
|
+
}
|
|
34
46
|
export function storeRecordedIds(store, system) {
|
|
35
47
|
return async (orgId) => {
|
|
36
48
|
const applied = await store.getAppliedState(orgId);
|
|
@@ -365,15 +377,22 @@ export class ProvidersCloudflareReader {
|
|
|
365
377
|
* resource per project and per project grant in a ZITADEL org (v1 scope —
|
|
366
378
|
* see that method's own doc comment), plus (#510) a synthesized `issuer`
|
|
367
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.
|
|
368
385
|
*/
|
|
369
386
|
export class ProvidersZitadelReader {
|
|
370
387
|
client;
|
|
371
388
|
resolveScope;
|
|
372
389
|
resolveRecordedIds;
|
|
373
|
-
|
|
390
|
+
resolveLoginHost;
|
|
391
|
+
constructor(client, resolveScope, resolveRecordedIds, resolveLoginHost = async () => null) {
|
|
374
392
|
this.client = client;
|
|
375
393
|
this.resolveScope = resolveScope;
|
|
376
394
|
this.resolveRecordedIds = resolveRecordedIds;
|
|
395
|
+
this.resolveLoginHost = resolveLoginHost;
|
|
377
396
|
}
|
|
378
397
|
async listResources(orgId) {
|
|
379
398
|
const zitadelOrgId = await this.resolveScope(orgId);
|
|
@@ -417,7 +436,11 @@ export class ProvidersZitadelReader {
|
|
|
417
436
|
return null;
|
|
418
437
|
throw err;
|
|
419
438
|
}
|
|
420
|
-
|
|
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 };
|
|
421
444
|
}
|
|
422
445
|
}
|
|
423
446
|
/**
|
package/dist/plan/step-system.js
CHANGED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { OrgId } from '../org.js';
|
|
2
|
+
import type { VersionChangeItem } from '../plan.js';
|
|
3
|
+
/**
|
|
4
|
+
* #263: a read-only view of which `@wtfalch/*` versions each org's hosted
|
|
5
|
+
* app is actually running, beside the platform's own version-change lines
|
|
6
|
+
* (`plan.ts`'s {@link VersionChangeItem}, #251) -- the data behind Archon's
|
|
7
|
+
* "Shared platform page" (wtfalch/archon#133). Nothing here mutates
|
|
8
|
+
* anything; it only reads.
|
|
9
|
+
*
|
|
10
|
+
* The problem this exists for: a shared service (ZITADEL, Synapse, the
|
|
11
|
+
* jobs scheduler, ...) has one declared version on `DeclaredPlatform`, but
|
|
12
|
+
* each org's own hosted app pulls its `@wtfalch/*` dependencies from
|
|
13
|
+
* whatever its own `pnpm-lock.yaml` pinned the day it was last deployed --
|
|
14
|
+
* and nothing before this listed who is actually on what.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* One org app this reads status for. The caller resolves `repo` and
|
|
18
|
+
* `coolifyApplicationUuid` itself, the same way every other reader in this
|
|
19
|
+
* package is handed an already-resolved scope id rather than resolving one
|
|
20
|
+
* itself (`providers-readers.ts`'s `ScopeResolver` doc comment) --
|
|
21
|
+
* `coolifyApplicationUuid` is the id `plan/diff.ts`'s `appStepKey` keys an
|
|
22
|
+
* org's hosting entry under in `AppliedState.serviceIds`
|
|
23
|
+
* (`` `coolify:app:${appStepKey(org, entry.key)}` ``, `org.ts`); `repo` is
|
|
24
|
+
* `HostingEntry.repo`, falling back to whatever default an app with none
|
|
25
|
+
* declared resolves to (`plan/executor.ts`'s own `DEFAULT_APP_REPO`).
|
|
26
|
+
*/
|
|
27
|
+
export interface OrgAppVersionRef {
|
|
28
|
+
orgId: OrgId;
|
|
29
|
+
orgSlug: string;
|
|
30
|
+
/** The hosting entry's own key, e.g. `'app'` (`org.ts`'s `APP_ENTRY_KEY`). */
|
|
31
|
+
entryKey: string;
|
|
32
|
+
/** The repo under `wtfalch` this app builds from, e.g. `'wtfalch/lokessmie'`. */
|
|
33
|
+
repo: string;
|
|
34
|
+
coolifyApplicationUuid: string;
|
|
35
|
+
}
|
|
36
|
+
/** One org app's read status: what is actually deployed, and what it depends on. */
|
|
37
|
+
export interface OrgAppVersionStatus {
|
|
38
|
+
orgId: OrgId;
|
|
39
|
+
orgSlug: string;
|
|
40
|
+
entryKey: string;
|
|
41
|
+
repo: string;
|
|
42
|
+
/**
|
|
43
|
+
* The full 40-character commit sha Coolify's own deployment history
|
|
44
|
+
* reports as currently deployed (Choice A, #263's "Decide": Coolify's
|
|
45
|
+
* application record is live truth over foundry's own last-applied
|
|
46
|
+
* record, at the cost of a Coolify call this function always makes).
|
|
47
|
+
* `null` when there is no finished deployment yet -- a brand new app, or
|
|
48
|
+
* one whose only deploy so far failed.
|
|
49
|
+
*/
|
|
50
|
+
deployedSha: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* `@wtfalch/*` package name to resolved version, read from this app's
|
|
53
|
+
* `pnpm-lock.yaml` at {@link deployedSha}. `null` when there is nothing to
|
|
54
|
+
* read it from yet (`deployedSha` is `null`) or the read itself failed --
|
|
55
|
+
* {@link error} says which.
|
|
56
|
+
*/
|
|
57
|
+
wtfalchVersions: Record<string, string> | null;
|
|
58
|
+
/** Set when this app's status could not be fully read; `wtfalchVersions` is `null` in this case too. */
|
|
59
|
+
error: string | null;
|
|
60
|
+
}
|
|
61
|
+
/** Across every app a call named, one `@wtfalch/*` package's spread of versions. */
|
|
62
|
+
export interface WtfalchPackageDrift {
|
|
63
|
+
package: string;
|
|
64
|
+
/** Every distinct version seen, each with the org slugs running it, so a human reads who's on what with no cross-reference against `apps`. */
|
|
65
|
+
versionsBySlug: Record<string, string[]>;
|
|
66
|
+
/**
|
|
67
|
+
* The version a caller declared as the intended one for this package, if
|
|
68
|
+
* it passed one in `declaredVersions` (#263's "or from the declared
|
|
69
|
+
* one") -- never guessed here: this package has no fixed mapping from a
|
|
70
|
+
* `PlatformServiceKey` to an npm package name, so a caller that has one
|
|
71
|
+
* (or none) decides.
|
|
72
|
+
*/
|
|
73
|
+
declaredVersion?: string;
|
|
74
|
+
/** True once more than one version is in use across `apps`, or an app's version differs from `declaredVersion`. */
|
|
75
|
+
drifted: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** The combined view {@link readPlatformVersionStatus} returns. */
|
|
78
|
+
export interface PlatformVersionStatus {
|
|
79
|
+
apps: OrgAppVersionStatus[];
|
|
80
|
+
/** Sorted by package name. */
|
|
81
|
+
packageDrift: WtfalchPackageDrift[];
|
|
82
|
+
/**
|
|
83
|
+
* The platform's own version-change lines (#251), carried through
|
|
84
|
+
* unchanged so a caller can render them on the same page with one call
|
|
85
|
+
* instead of two -- `toPlatformPlan`'s own `Plan.versionChanges`, or
|
|
86
|
+
* `PlatformPlanDetail.versionChanges` directly.
|
|
87
|
+
*/
|
|
88
|
+
platformVersionChanges: VersionChangeItem[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Reads one application's currently deployed commit sha.
|
|
92
|
+
*
|
|
93
|
+
* Narrow on purpose, the same shape every other port in `executor.ts`
|
|
94
|
+
* takes (`CoolifyPort`, `CloudflarePort`, ...): a real implementation is
|
|
95
|
+
* {@link providersStatusCoolifyPort}, over `@wtfalch/providers/coolify`'s
|
|
96
|
+
* `CoolifyClient`; a fake satisfies this directly in a test.
|
|
97
|
+
*/
|
|
98
|
+
export interface StatusCoolifyPort {
|
|
99
|
+
/**
|
|
100
|
+
* `null` when there is no *finished* deployment yet
|
|
101
|
+
* (`@wtfalch/providers/coolify`'s own doc comment on
|
|
102
|
+
* `CoolifyDeploymentRecord`: an application's bare `status` field reports
|
|
103
|
+
* on the container that is currently *running*, not on whether the last
|
|
104
|
+
* deploy actually landed, so a failed build leaves the previous image
|
|
105
|
+
* running and healthy with nothing in that field to say so -- this reads
|
|
106
|
+
* the deployment history instead, exactly as that doc comment advises).
|
|
107
|
+
*/
|
|
108
|
+
deployedSha(applicationUuid: string): Promise<string | null>;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Reads one file's text contents in a GitHub repo, at a git ref.
|
|
112
|
+
*
|
|
113
|
+
* `@wtfalch/providers` 0.6.0's `GitHubClient` (`dist/github.d.ts`) has no
|
|
114
|
+
* such method yet, and this repo cannot add one to that published package
|
|
115
|
+
* from here -- so this is foundry's own minimal port for exactly the one
|
|
116
|
+
* capability #263 needs, built the same injectable way `GitHubClient`
|
|
117
|
+
* itself is (its own `GhExec`), so a host can later swap in a real
|
|
118
|
+
* `providers` method with no change to anything that depends on this
|
|
119
|
+
* interface.
|
|
120
|
+
*/
|
|
121
|
+
export interface StatusGithubPort {
|
|
122
|
+
/** `null` when `path` does not exist at `ref` (or `ref` itself does not exist). */
|
|
123
|
+
getFileContents(repo: string, path: string, ref: string): Promise<string | null>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extracts every `@wtfalch/*` package's resolved version out of a pnpm
|
|
127
|
+
* lockfile's text, without parsing YAML in general.
|
|
128
|
+
*
|
|
129
|
+
* pnpm lockfile v9 (`lockfileVersion: '9.0'`, this estate's own) writes each
|
|
130
|
+
* resolved package as a two-space-indented, single-quoted
|
|
131
|
+
* `'<name>@<version>'` key in its `packages:` section, and again --
|
|
132
|
+
* peer-qualified, e.g. `'<name>@<version>(<peer>@<peer-version>...)'` -- in
|
|
133
|
+
* its `snapshots:` section. Both carry the same base version for a given
|
|
134
|
+
* package, so reading either (or both, as this does) and keeping the last
|
|
135
|
+
* value seen per package name is lossless for what this exists to compare:
|
|
136
|
+
* *which version* an app is on, never which peer graph it resolved to.
|
|
137
|
+
* Verified against this very repo's own `pnpm-lock.yaml`.
|
|
138
|
+
*/
|
|
139
|
+
export declare function wtfalchVersionsFromPnpmLockfile(lockfile: string): Record<string, string>;
|
|
140
|
+
export interface ReadPlatformVersionStatusOptions {
|
|
141
|
+
/** Default `'pnpm-lock.yaml'`. */
|
|
142
|
+
lockfilePath?: string;
|
|
143
|
+
/**
|
|
144
|
+
* The intended version for a package, keyed by its `@wtfalch/*` name --
|
|
145
|
+
* #263's "or from the declared one". Optional: without it, drift is
|
|
146
|
+
* computed across `apps` alone.
|
|
147
|
+
*/
|
|
148
|
+
declaredVersions?: Readonly<Record<string, string>>;
|
|
149
|
+
/** Carried straight through onto {@link PlatformVersionStatus.platformVersionChanges}; defaults to none. */
|
|
150
|
+
platformVersionChanges?: readonly VersionChangeItem[];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Reads every named app's deployed sha and the `@wtfalch/*` versions its
|
|
154
|
+
* lockfile pins at that sha, then flags which packages have drifted --
|
|
155
|
+
* across the named apps, or against a declared version when the caller
|
|
156
|
+
* passes one (#263's "Done when": two orgs on different `@wtfalch/auth`
|
|
157
|
+
* versions is flagged).
|
|
158
|
+
*
|
|
159
|
+
* One app's read failing (its sha cannot be found, or its lockfile fetch
|
|
160
|
+
* errors) is recorded on that app alone (`OrgAppVersionStatus.error`) and
|
|
161
|
+
* never stops another app's read, or the drift computed from whichever
|
|
162
|
+
* apps did resolve.
|
|
163
|
+
*/
|
|
164
|
+
export declare function readPlatformVersionStatus(apps: readonly OrgAppVersionRef[], ports: {
|
|
165
|
+
coolify: StatusCoolifyPort;
|
|
166
|
+
github: StatusGithubPort;
|
|
167
|
+
}, options?: ReadPlatformVersionStatusOptions): Promise<PlatformVersionStatus>;
|
|
168
|
+
/**
|
|
169
|
+
* The real {@link StatusCoolifyPort}, over `@wtfalch/providers/coolify`'s
|
|
170
|
+
* `CoolifyClient` (Choice A, #263's "Decide"). Narrowed to the one method
|
|
171
|
+
* this needs, the same `Pick`-shaped narrowing nothing else in this package
|
|
172
|
+
* happened to need before -- `CoolifyClient` itself is never imported as a
|
|
173
|
+
* type here, so a fake in a test needs no part of its surface this doesn't use.
|
|
174
|
+
*/
|
|
175
|
+
export declare function providersStatusCoolifyPort(client: {
|
|
176
|
+
listApplicationDeployments(uuid: string, take?: number): Promise<Array<{
|
|
177
|
+
commit: string;
|
|
178
|
+
status: string;
|
|
179
|
+
}>>;
|
|
180
|
+
}): StatusCoolifyPort;
|
|
181
|
+
/**
|
|
182
|
+
* The `gh` CLI call {@link ghCliStatusGithubPort} makes -- same shape as
|
|
183
|
+
* `@wtfalch/providers/github`'s own `GhExec`, so a host that already has a
|
|
184
|
+
* real one wired for `GitHubClient` can hand the identical function to
|
|
185
|
+
* both. Injectable so a test never spawns a real process.
|
|
186
|
+
*/
|
|
187
|
+
export type GhExec = (args: string[]) => Promise<{
|
|
188
|
+
stdout: string;
|
|
189
|
+
stderr: string;
|
|
190
|
+
}>;
|
|
191
|
+
/**
|
|
192
|
+
* The real {@link StatusGithubPort}, built on `gh api repos/{repo}/contents/{path}`
|
|
193
|
+
* the way GitHub's own REST API serves a file's contents at a ref --
|
|
194
|
+
* `{ content: '<base64>' }` on success, a non-zero exit with `404` in `gh`'s
|
|
195
|
+
* own error text when `path` or `ref` doesn't exist (checked against a real
|
|
196
|
+
* `gh` CLI, not GitHub's docs, matching this package's `coolify.ts`'s own
|
|
197
|
+
* "verified against a live instance, not the docs" standard for an API this
|
|
198
|
+
* cannot run against the network in CI).
|
|
199
|
+
*/
|
|
200
|
+
export declare function ghCliStatusGithubPort(exec?: GhExec): StatusGithubPort;
|