@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.
@@ -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
- if (await filesRunnerEnvDrifted(this.files.coolify, recordedIds, {
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`/`wtfalch.dev` replaced by this
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.wtfalch.dev`: that plan also wires an `AcmeProvider` and `DnsServer`
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 `wtfalch.dev`
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.wtfalch.dev` stays a human step.
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.map((e) => (e.value.includes('\n') ? { ...e, multiline: true } : e)));
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 `wtfalch <noreply@wtfalch.dev>` for an entry with no
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
- // #436 round 2: not `PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY`'s
4025
- // internal `http://<name>:3000` -- Archon's signed path needs an
4026
- // exact HTTPS origin (`filesOrigin()`/`filesBaseUrl()`,
4027
- // `wtfalch/archon`'s `src/lib/authz/files-management.ts` and
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), and the three
5361
- * {@link PLATFORM_FILES_RUNNER_DEFAULTS} (#380, written only when absent).
5362
- * `AUTHZ_BOOTSTRAP_OPERATOR` and the rest of files' `.env.example` are
5363
- * still set by a person on the runner.
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
- constructor(client: ZitadelClient, resolveScope: ScopeResolver, resolveRecordedIds: RecordedIds);
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
- constructor(client, resolveScope, resolveRecordedIds) {
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
- return { system: 'zitadel', kind: 'issuer', key: orgId, config: {} };
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
  /**
@@ -15,6 +15,7 @@ export function stepSystemOf(action) {
15
15
  case 'chat':
16
16
  case 'calls':
17
17
  case 'calls-service':
18
+ case 'calls-chat':
18
19
  return 'matrix';
19
20
  case 'database':
20
21
  case 'adopt-database':
@@ -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;