@wtfalch/foundry 0.29.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/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 +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 +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 +3 -2
|
@@ -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
|
|
@@ -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;
|