@wtfalch/foundry 0.74.0 → 0.76.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.
@@ -8,6 +8,13 @@ import { type JobRuntime } from './job-app.js';
8
8
  */
9
9
  export declare const MCP_DATABASE_URL_ENV = "MCP_DATABASE_URL";
10
10
  export declare const MCP_OAUTH_SECRET_ENV = "MCP_OAUTH_SECRET";
11
+ /**
12
+ * The key that seals the broker's shared auth state (`wtfalch/auth` broker/src/auth/state-config.ts). The
13
+ * broker runs in shared mode whenever `MCP_DATABASE_URL` is set and exits at boot without this key: at
14
+ * least 32 bytes, the same on every replica, never regenerated. `AUTH_STATE_SECRET_PREVIOUS` (rotation)
15
+ * is the operator's and is never touched here.
16
+ */
17
+ export declare const AUTH_STATE_SECRET_ENV = "AUTH_STATE_SECRET";
11
18
  /** The broker's own role and database on the sign-in Postgres. */
12
19
  export declare const BROKER_DB_NAME = "broker";
13
20
  export declare const BROKER_DB_USER = "broker";
@@ -11,6 +11,13 @@ import { SIGNIN_DB_CA_MOUNT, boundedCall } from './signin-db.js';
11
11
  */
12
12
  export const MCP_DATABASE_URL_ENV = 'MCP_DATABASE_URL';
13
13
  export const MCP_OAUTH_SECRET_ENV = 'MCP_OAUTH_SECRET';
14
+ /**
15
+ * The key that seals the broker's shared auth state (`wtfalch/auth` broker/src/auth/state-config.ts). The
16
+ * broker runs in shared mode whenever `MCP_DATABASE_URL` is set and exits at boot without this key: at
17
+ * least 32 bytes, the same on every replica, never regenerated. `AUTH_STATE_SECRET_PREVIOUS` (rotation)
18
+ * is the operator's and is never touched here.
19
+ */
20
+ export const AUTH_STATE_SECRET_ENV = 'AUTH_STATE_SECRET';
14
21
  /** The broker's own role and database on the sign-in Postgres. */
15
22
  export const BROKER_DB_NAME = 'broker';
16
23
  export const BROKER_DB_USER = 'broker';
@@ -84,9 +91,18 @@ export async function ensureBrokerMcpEnv(input) {
84
91
  throw new Error(`${input.label}: the broker lost its ${MCP_OAUTH_SECRET_ENV} between two reads`);
85
92
  }
86
93
  runtime.onSecret?.(secret);
94
+ // The same rule as the OAuth secret: minted once, a live value (hand-set too) is kept, and it is written
95
+ // wherever `MCP_DATABASE_URL` is, so a broker never has the one without the other.
96
+ const mintedState = await ensureGeneratedEnv(coolify, brokerAppId, AUTH_STATE_SECRET_ENV, () => randomBytes(32).toString('hex'), MCP_OAUTH_SECRET_MIN_LENGTH);
97
+ const stateSecret = mintedState ?? (await currentEnvValue(coolify, brokerAppId, AUTH_STATE_SECRET_ENV));
98
+ if (!stateSecret) {
99
+ throw new Error(`${input.label}: the broker lost its ${AUTH_STATE_SECRET_ENV} between two reads`);
100
+ }
101
+ runtime.onSecret?.(stateSecret);
87
102
  return [
88
103
  { key: MCP_DATABASE_URL_ENV, value: url },
89
104
  { key: MCP_OAUTH_SECRET_ENV, value: secret },
105
+ { key: AUTH_STATE_SECRET_ENV, value: stateSecret },
90
106
  ];
91
107
  }
92
108
  /** Runs the job once, waits for it, deletes it. Every error is fixed text with the app id, never a cause. */
package/dist/plan/diff.js CHANGED
@@ -48,7 +48,7 @@ import { OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY,
48
48
  import { ownDnsHostnames } from './own-dns.js';
49
49
  import { ownHostnames } from './own-hostnames.js';
50
50
  import { platformMailMxHost } from './platform-diff.js';
51
- import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appImageAppliedScopeKey, appSecretAvailability, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, entryImageRef, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
51
+ import { AUTH_MCP_ENV_VERSION, CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appImageAppliedScopeKey, appSecretAvailability, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, entryImageRef, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
52
52
  import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
53
53
  import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
54
54
  import { companyAppPostLogoutUris, postLogoutUpdateOwed, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
@@ -4611,7 +4611,9 @@ export function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired, b
4611
4611
  // Auth ADR 0002: a broker given an `mcp` entry before it holds its database and secret exits at boot.
4612
4612
  // A company's own sign-in step sets them and records it; a shared org's broker is the platform's, whose
4613
4613
  // marker this plan cannot see, so `writeBrokerEntry` refuses there until the platform step has run.
4614
- if (auth.mcp && ownsServices(org) && recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] !== '1') {
4614
+ if (auth.mcp &&
4615
+ ownsServices(org) &&
4616
+ recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] !== AUTH_MCP_ENV_VERSION) {
4615
4617
  blocked.push({
4616
4618
  description: `${at} is blocked, not planned: the broker's MCP database and secret are not set yet (the sign-in step sets them)`,
4617
4619
  key,
@@ -13,7 +13,7 @@ import { ownHostnames, passkeyRpIdFor } from './own-hostnames.js';
13
13
  import { JOBS_PUBLIC_URL_SCOPE_KEY, OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY } from './own-jobs-route.js';
14
14
  import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
15
15
  import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
16
- import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_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_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_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_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
16
+ import { AUTH_ISSUER_SOURCE_ENV_VERSION, AUTH_MCP_ENV_VERSION, AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_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_ISSUER_SOURCE_ENV_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_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_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
17
17
  import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
18
18
  import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
19
19
  import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn, signInLoginNames } from './sign-in.js';
@@ -266,6 +266,8 @@ export function planOwnAuth(options) {
266
266
  recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY] &&
267
267
  recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
268
268
  recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
269
+ // The login apps' source link settings; a stack from before them is written once.
270
+ recordedIds[PLATFORM_AUTH_ISSUER_SOURCE_ENV_SCOPE_KEY] === AUTH_ISSUER_SOURCE_ENV_VERSION &&
269
271
  // The login apps' mail env (`LOGIN_MAIL_FROM`, the Lettermint key); a stack from before it is written again.
270
272
  recordedIds[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] ===
271
273
  authLoginEnvMarker(`noreply@${names.mailDomain}`, passkeyRpIdFor(org)) &&
@@ -273,7 +275,7 @@ export function planOwnAuth(options) {
273
275
  (options.signinMailWritable === false ||
274
276
  recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] === signinMailMarker(org.displayName)) &&
275
277
  // An entry with `mcp` needs the broker's database and secret (auth ADR 0002).
276
- (!orgDeclaresMcp(org) || recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] === '1') &&
278
+ (!orgDeclaresMcp(org) || recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] === AUTH_MCP_ENV_VERSION) &&
277
279
  (Boolean(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]) || !brokerHoldsAnEntry) &&
278
280
  // In-app sign-in: the second login app is recorded once it is deployed and holds the token, which
279
281
  // needs the login client; until the broker holds an entry that cannot be, so the step waits.
@@ -87,11 +87,26 @@ export declare function authLoginEnvMarker(smtpSender: string, passkeyRpId?: str
87
87
  /** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
88
88
  export declare const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = "auth_login_env_applied";
89
89
  /**
90
- * `'1'` once the broker holds `MCP_DATABASE_URL` and `MCP_OAUTH_SECRET` (auth ADR 0002), recorded by the
90
+ * The version of the two settings the sign-in app's source link reads (`ISSUER_SOURCE_URL`,
91
+ * `ISSUER_SOURCE_LINK_SECRET`). Raise it to write them again. They are kept apart from
92
+ * {@link AUTH_LOGIN_ENV_VERSION} on purpose: a change to the marker redeploys both login apps, and
93
+ * these two settings must not.
94
+ * 1: the login apps get both settings (foundry 0.76.0).
95
+ */
96
+ export declare const AUTH_ISSUER_SOURCE_ENV_VERSION = "1";
97
+ /** The {@link AUTH_ISSUER_SOURCE_ENV_VERSION} the login apps' source link settings were last written with. */
98
+ export declare const PLATFORM_AUTH_ISSUER_SOURCE_ENV_SCOPE_KEY = "auth_issuer_source_env_applied";
99
+ /**
100
+ * `AUTH_MCP_ENV_VERSION` once the broker holds `MCP_DATABASE_URL`, `MCP_OAUTH_SECRET` and `AUTH_STATE_SECRET` (auth ADR 0002), recorded by the
91
101
  * sign-in step when an entry of that broker declares `uses.auth.mcp`. A broker entry with `mcp` is not
92
102
  * written before it, and an applied sign-in that wants it and lacks it plans its step again.
93
103
  */
94
104
  export declare const PLATFORM_AUTH_MCP_SCOPE_KEY = "auth_mcp_env_applied";
105
+ /**
106
+ * The value of that marker. `'2'` since the broker also needs `AUTH_STATE_SECRET` (auth PR 510), so a
107
+ * broker applied under `'1'` plans its sign-in step once more and gets the key without a hand step.
108
+ */
109
+ export declare const AUTH_MCP_ENV_VERSION = "2";
95
110
  /** True when a hosting entry of the org declares `uses.auth.mcp`: its broker needs the MCP database and secret. */
96
111
  export declare function orgDeclaresMcp(org: Pick<DeclaredOrg, 'hosting'>): boolean;
97
112
  /**
@@ -96,11 +96,26 @@ export function authLoginEnvMarker(smtpSender, passkeyRpId) {
96
96
  /** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
97
97
  export const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = 'auth_login_env_applied';
98
98
  /**
99
- * `'1'` once the broker holds `MCP_DATABASE_URL` and `MCP_OAUTH_SECRET` (auth ADR 0002), recorded by the
99
+ * The version of the two settings the sign-in app's source link reads (`ISSUER_SOURCE_URL`,
100
+ * `ISSUER_SOURCE_LINK_SECRET`). Raise it to write them again. They are kept apart from
101
+ * {@link AUTH_LOGIN_ENV_VERSION} on purpose: a change to the marker redeploys both login apps, and
102
+ * these two settings must not.
103
+ * 1: the login apps get both settings (foundry 0.76.0).
104
+ */
105
+ export const AUTH_ISSUER_SOURCE_ENV_VERSION = '1';
106
+ /** The {@link AUTH_ISSUER_SOURCE_ENV_VERSION} the login apps' source link settings were last written with. */
107
+ export const PLATFORM_AUTH_ISSUER_SOURCE_ENV_SCOPE_KEY = 'auth_issuer_source_env_applied';
108
+ /**
109
+ * `AUTH_MCP_ENV_VERSION` once the broker holds `MCP_DATABASE_URL`, `MCP_OAUTH_SECRET` and `AUTH_STATE_SECRET` (auth ADR 0002), recorded by the
100
110
  * sign-in step when an entry of that broker declares `uses.auth.mcp`. A broker entry with `mcp` is not
101
111
  * written before it, and an applied sign-in that wants it and lacks it plans its step again.
102
112
  */
103
113
  export const PLATFORM_AUTH_MCP_SCOPE_KEY = 'auth_mcp_env_applied';
114
+ /**
115
+ * The value of that marker. `'2'` since the broker also needs `AUTH_STATE_SECRET` (auth PR 510), so a
116
+ * broker applied under `'1'` plans its sign-in step once more and gets the key without a hand step.
117
+ */
118
+ export const AUTH_MCP_ENV_VERSION = '2';
104
119
  /** True when a hosting entry of the org declares `uses.auth.mcp`: its broker needs the MCP database and secret. */
105
120
  export function orgDeclaresMcp(org) {
106
121
  return org.hosting.enabled && org.hosting.entries.some((e) => Boolean(e.uses?.auth?.mcp));
@@ -2295,6 +2310,8 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
2295
2310
  // step's redeploy, so a platform recorded before them still plans it.
2296
2311
  recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
2297
2312
  recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
2313
+ // The login apps' source link settings: a platform from before them plans the step once.
2314
+ recordedIds[PLATFORM_AUTH_ISSUER_SOURCE_ENV_SCOPE_KEY] === AUTH_ISSUER_SOURCE_ENV_VERSION &&
2298
2315
  // The login apps' mail env: a platform from before it, or with a new apex, is written again.
2299
2316
  // Without the held key the apply cannot record it, so it is asked for only once the key is held.
2300
2317
  (!mailKeyHeld ||
@@ -1420,6 +1420,22 @@ export declare function brokerDeployNeeded(missingInputs: readonly string[], bro
1420
1420
  * wrote there. Paired with {@link brokerDeployNeeded}, which reads it back.
1421
1421
  */
1422
1422
  export declare function brokerDeployRecordValue(brokerAppId: string, authAppsRaw: string | undefined): string;
1423
+ /**
1424
+ * The held master key of the sign-in app's source link: one value the platform holds. It is read in foundry
1425
+ * only and never reaches a container, plan line, log or error; each company gets a key derived from it
1426
+ * ({@link issuerSourceLinkKey}). Never minted here; a person (or the host) puts it under this ref, and a
1427
+ * missing value only hides the link.
1428
+ */
1429
+ export declare const PLATFORM_AUTH_ISSUER_SOURCE_LINK_SECRET_REF = "issuer-source-link-secret";
1430
+ /** The sign-in app's source link settings. The URL is public; the key never reaches a plan line, log or error. */
1431
+ export declare const LOGIN_ISSUER_SOURCE_URL_ENV = "ISSUER_SOURCE_URL";
1432
+ export declare const LOGIN_ISSUER_SOURCE_LINK_SECRET_ENV = "ISSUER_SOURCE_LINK_SECRET";
1433
+ /** The company id of a source link: the target's id (`platform`, or the org's slug), the one its internal names use. */
1434
+ export declare const ISSUER_SOURCE_COMPANY_ID: RegExp;
1435
+ /** The source link of one company. */
1436
+ export declare function issuerSourceUrl(company: string): string;
1437
+ /** A company's source link key: lowercase hex of HMAC-SHA256(master, company). */
1438
+ export declare function issuerSourceLinkKey(master: string, company: string): string;
1423
1439
  /** Whether any desired env is missing from the app, or set to another value. */
1424
1440
  export declare function envsDiffer(coolify: Pick<CoolifyPort, 'listEnvs'>, appId: string, desired: Array<{
1425
1441
  key: string;
@@ -1,4 +1,4 @@
1
- import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, randomUUID, sign, } from 'node:crypto';
1
+ import { createHash, createHmac, createPrivateKey, generateKeyPairSync, randomBytes, randomUUID, sign, } from 'node:crypto';
2
2
  import { isIPv4 } from 'node:net';
3
3
  import { applyObjects, openInstance, platformMailSettingsObjects } from '@wtfalch/email/postmaster';
4
4
  import { VERSIONS, coolifyImageFor, floatingTagBlocked, pinnedRef } from '../bootstrap/versions.js';
@@ -34,7 +34,7 @@ import { MAIL_APP_SCOPE_KEY, MAIL_DATABASE_NAME, MAIL_DATABASE_PASSWORD_ENV, MAI
34
34
  import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
35
35
  import { recordOwnDnsHostname } from './own-dns.js';
36
36
  import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
37
- import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, GITHUB_RELEASE_HOOK_DEPS_MISSING, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, 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_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_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_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_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_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_APPLIED_GIT_REF_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_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_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_TUNNEL_ROUTES_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_APPLIED_USAGE_TAIL_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_GITHUB_RELEASE_HOOK_KEY, PLATFORM_HC_ACCESS_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_HEARTBEATS_HOSTED_API_KEY_REF, 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_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_USER_ROLE, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_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_AUDIT_SEAL_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_USAGE_TAIL_KEY, PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, PLATFORM_USAGE_TAIL_SCRIPT_NAME, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, authLoginEnvMarker, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeTicksSyncValue, uptimeVarsDigest, usageTailEdgeEnv, usageTailEdgeEnvValue, usageTailRecorded, } from './platform-diff.js';
37
+ import { AUTH_ISSUER_SOURCE_ENV_VERSION, AUTH_MCP_ENV_VERSION, AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, GITHUB_RELEASE_HOOK_DEPS_MISSING, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, 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_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_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_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_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_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_APPLIED_GIT_REF_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_ISSUER_SOURCE_ENV_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_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_TUNNEL_ROUTES_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_APPLIED_USAGE_TAIL_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_GITHUB_RELEASE_HOOK_KEY, PLATFORM_HC_ACCESS_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_HEARTBEATS_HOSTED_API_KEY_REF, 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_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_USER_ROLE, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_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_AUDIT_SEAL_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_USAGE_TAIL_KEY, PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, PLATFORM_USAGE_TAIL_SCRIPT_NAME, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, authLoginEnvMarker, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeTicksSyncValue, uptimeVarsDigest, usageTailEdgeEnv, usageTailEdgeEnvValue, usageTailRecorded, } from './platform-diff.js';
38
38
  import { PLATFORM_REGISTRY_KEY, PLATFORM_REGISTRY_LOGINS_KEY, PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY, PLATFORM_REGISTRY_PULL_PASSWORD_REF, registryImageTagBlocked, } from './platform-registry-keys.js';
39
39
  import { ensureRegistryLogins, provisionPlatformRegistry, registryLoginsDrifted, } from './platform-registry.js';
40
40
  import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
@@ -914,7 +914,8 @@ export class FoundryPlatformStepExecutor {
914
914
  if (platform.auth.enabled && this.auth) {
915
915
  const loginClientDrifted = await authLoginClientDrifted(platform, this.auth, recordedIds);
916
916
  // The broker lacks the MCP database and secret a shared org's entry needs (auth ADR 0002).
917
- const mcpDrifted = recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] !== '1' && (await this.mcpWanted());
917
+ const mcpDrifted = recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] !== AUTH_MCP_ENV_VERSION &&
918
+ (await this.mcpWanted());
918
919
  // The broker has no registrar token yet (`ZITADEL_REGISTRAR_TOKEN`).
919
920
  // A token minted and never held is blocked (`blockedSteps`), not drift: no apply could change it.
920
921
  const registrarDrifted = this.auth.registrarTokenWritable === true &&
@@ -1503,8 +1504,8 @@ export const PLATFORM_AUTH_PROJECT = 'platform';
1503
1504
  * header and so cannot give two different apps the same public hostname.
1504
1505
  */
1505
1506
  export const PLATFORM_AUTH_LOGIN_INTERNAL_NAME = 'platform-auth-login';
1506
- /** `wtfalch/auth`'s `docker-compose.yml`: `image: ghcr.io/zitadel/zitadel:${ZITADEL_VERSION:-v4.17.3}`. Pinned in `versions.json` (#247). */
1507
- const AUTH_ZITADEL_IMAGE = 'ghcr.io/zitadel/zitadel';
1507
+ /** The patched sign-in server (stock ZITADEL plus our patches; it keeps `/app/zitadel`). Private package: the Coolify server pulls it with its own login. Pinned in `versions.json` (#247). */
1508
+ const AUTH_ZITADEL_IMAGE = 'ghcr.io/wtfalch/auth-issuer';
1508
1509
  const AUTH_ZITADEL_IMAGE_TAG = VERSIONS.zitadel;
1509
1510
  /** `wtfalch/auth`'s own `login/Dockerfile` -- `docs/deploy.md`'s "One compose service, four containers... Two images are built from this repository: the proxy... and the login". */
1510
1511
  const AUTH_REPO = 'wtfalch/auth';
@@ -2263,6 +2264,36 @@ export async function provisionAuthStack(scope, deps, target, input) {
2263
2264
  loginEnvs.push({ key: 'LOGIN_MAIL_FROM', value: input.smtpSender });
2264
2265
  if (input.passkeyRpId)
2265
2266
  loginEnvs.push({ key: LOGIN_PASSKEY_RP_ID_ENV, value: input.passkeyRpId });
2267
+ // The sign-in app's source link: both settings, or neither. A master that is not held, or cannot be read,
2268
+ // hides the link and fails nothing; the marker stays unset, so the step is planned again once it is held.
2269
+ let issuerSourceSecret = null;
2270
+ let issuerSourceReadFailed = false;
2271
+ try {
2272
+ issuerSourceSecret =
2273
+ (await deps.heldKeys?.resolve(PLATFORM_AUTH_ISSUER_SOURCE_LINK_SECRET_REF)) ?? null;
2274
+ }
2275
+ catch {
2276
+ // No cause in the line: a failed read can carry anything.
2277
+ issuerSourceReadFailed = true;
2278
+ }
2279
+ let issuerSourceWritten = false;
2280
+ if (issuerSourceSecret && !ISSUER_SOURCE_COMPANY_ID.test(target.id)) {
2281
+ console.info(`${label}: ${target.id} is not a valid source link company id, so the sign-in app hides the source link`);
2282
+ }
2283
+ else if (issuerSourceSecret) {
2284
+ // The master key stays in foundry: the container gets a key derived for this company only.
2285
+ runtime.onSecret?.(issuerSourceSecret);
2286
+ const companyKey = issuerSourceLinkKey(issuerSourceSecret, target.id);
2287
+ runtime.onSecret?.(companyKey);
2288
+ loginEnvs.push({ key: LOGIN_ISSUER_SOURCE_URL_ENV, value: issuerSourceUrl(target.id) }, { key: LOGIN_ISSUER_SOURCE_LINK_SECRET_ENV, value: companyKey });
2289
+ issuerSourceWritten = true;
2290
+ }
2291
+ else if (issuerSourceReadFailed) {
2292
+ console.info(`${label}: reading the held key ${PLATFORM_AUTH_ISSUER_SOURCE_LINK_SECRET_REF} failed, so the sign-in app hides the source link for now`);
2293
+ }
2294
+ else {
2295
+ console.info(`${label}: the held key ${PLATFORM_AUTH_ISSUER_SOURCE_LINK_SECRET_REF} is not held, so the sign-in app hides the source link`);
2296
+ }
2266
2297
  // `AUTH_APPS` and `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` are not pushed here
2267
2298
  // -- this function's own doc comment, "Known gaps". `LETTERMINT_API_TOKEN`
2268
2299
  // is, once held (#210): pushing it costs nothing even while the broker
@@ -2299,7 +2330,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
2299
2330
  // container on its next deploy (`diff.ts`'s `coolifyCalls` follows the
2300
2331
  // identical rule).
2301
2332
  const zitadelEnvsChanged = await envsDiffer(deps.coolify, zitadelAppId, zitadelEnvs);
2302
- const loginEnvsChanged = await envsDiffer(deps.coolify, loginAppId, loginEnvs);
2333
+ const loginEnvsChanged = await envsDiffer(deps.coolify, loginAppId, withoutIssuerSourceEnvs(loginEnvs));
2303
2334
  const brokerEnvsChanged = await envsDiffer(deps.coolify, brokerAppId, brokerEnvs);
2304
2335
  await deps.coolify.bulkUpsertEnvs(zitadelAppId, zitadelEnvs);
2305
2336
  await deps.coolify.bulkUpsertEnvs(loginAppId, loginEnvs);
@@ -2331,7 +2362,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
2331
2362
  { key: SIGN_IN_BIND_KEY_ENV, value: bindKey },
2332
2363
  ...(bindings ? [{ key: LOGIN_HOST_BINDINGS_ENV, value: bindings }] : []),
2333
2364
  ];
2334
- signInEnvsChanged = await envsDiffer(deps.coolify, signInApp.appId, signInEnvs);
2365
+ signInEnvsChanged = await envsDiffer(deps.coolify, signInApp.appId, withoutIssuerSourceEnvs(signInEnvs));
2335
2366
  await deps.coolify.bulkUpsertEnvs(signInApp.appId, signInEnvs);
2336
2367
  staleRpIdSignIn = input.passkeyRpId
2337
2368
  ? false
@@ -2387,6 +2418,11 @@ export async function provisionAuthStack(scope, deps, target, input) {
2387
2418
  if (input.signInLogin)
2388
2419
  await record(SIGN_IN_BIND_KEY_SCOPE_KEY, '1');
2389
2420
  }
2421
+ // The source link settings are written on both login apps and need no deploy of their own. Recorded only
2422
+ // when they were written, so a company without the master is planned again once it is held.
2423
+ if (issuerSourceWritten) {
2424
+ await record(PLATFORM_AUTH_ISSUER_SOURCE_ENV_SCOPE_KEY, AUTH_ISSUER_SOURCE_ENV_VERSION);
2425
+ }
2390
2426
  // The mail env is live on the login apps: written above and deployed with them. Without the held
2391
2427
  // key the marker stays unset, so the step is planned again once the key is held.
2392
2428
  if (heldMailKey && input.smtpSender) {
@@ -2420,7 +2456,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
2420
2456
  }
2421
2457
  // The env is live on the broker (and deployed when it changed): the entry write may now carry `mcp`.
2422
2458
  if (input.mcp)
2423
- await record(PLATFORM_AUTH_MCP_SCOPE_KEY, '1');
2459
+ await record(PLATFORM_AUTH_MCP_SCOPE_KEY, AUTH_MCP_ENV_VERSION);
2424
2460
  // ---- tunnel and DNS ------------------------------------------------------
2425
2461
  const tunnels = await network.tunnels();
2426
2462
  const existingTunnel = await tunnels.findTunnelByName(target.names.authTunnel);
@@ -7652,6 +7688,33 @@ export function brokerDeployRecordValue(brokerAppId, authAppsRaw) {
7652
7688
  return JSON.stringify(record);
7653
7689
  }
7654
7690
  const LOGIN_PASSKEY_RP_ID_ENV = 'LOGIN_PASSKEY_RP_ID';
7691
+ /**
7692
+ * The held master key of the sign-in app's source link: one value the platform holds. It is read in foundry
7693
+ * only and never reaches a container, plan line, log or error; each company gets a key derived from it
7694
+ * ({@link issuerSourceLinkKey}). Never minted here; a person (or the host) puts it under this ref, and a
7695
+ * missing value only hides the link.
7696
+ */
7697
+ export const PLATFORM_AUTH_ISSUER_SOURCE_LINK_SECRET_REF = 'issuer-source-link-secret';
7698
+ /** The sign-in app's source link settings. The URL is public; the key never reaches a plan line, log or error. */
7699
+ export const LOGIN_ISSUER_SOURCE_URL_ENV = 'ISSUER_SOURCE_URL';
7700
+ export const LOGIN_ISSUER_SOURCE_LINK_SECRET_ENV = 'ISSUER_SOURCE_LINK_SECRET';
7701
+ /** The company id of a source link: the target's id (`platform`, or the org's slug), the one its internal names use. */
7702
+ export const ISSUER_SOURCE_COMPANY_ID = /^[a-z0-9][a-z0-9_-]{0,62}$/;
7703
+ /** The source link of one company. */
7704
+ export function issuerSourceUrl(company) {
7705
+ return `https://auth-issuer-source.wtfalch-business.workers.dev/c/${company}/sha256/1bc635d35b65f87d8c3224d76aecf301cb01730cd31e1612a5e4e6f78a8d59ed/auth-issuer-source.tar.gz`;
7706
+ }
7707
+ /** A company's source link key: lowercase hex of HMAC-SHA256(master, company). */
7708
+ export function issuerSourceLinkKey(master, company) {
7709
+ return createHmac('sha256', master).update(company).digest('hex');
7710
+ }
7711
+ /**
7712
+ * Drops the source link settings from an env list before it is compared with the live env. Changing them
7713
+ * must not redeploy a login app: they reach a running app on its next deploy, and a new app gets them at create.
7714
+ */
7715
+ function withoutIssuerSourceEnvs(envs) {
7716
+ return envs.filter((e) => e.key !== LOGIN_ISSUER_SOURCE_URL_ENV && e.key !== LOGIN_ISSUER_SOURCE_LINK_SECRET_ENV);
7717
+ }
7655
7718
  /** Removes one env from an app when it is live. True when it was removed; false with nothing live or no delete on the port. */
7656
7719
  async function removeLoginEnv(coolify, appId, key) {
7657
7720
  if (typeof coolify.deleteEnv !== 'function')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.74.0",
3
+ "version": "0.76.0",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  {
2
- "zitadel": "sha256:2ec2a42551862ca59dc752c321c7041358dea8b33b63ea5e021ec499ad5e2d9f",
2
+ "zitadel": "sha256:67af7d205cbbae4c60ce90c5034521fbb73a711a49bffec6aa719927ca77a14f",
3
3
  "synapse": "sha256:6b95dd129e35e97e8032cb759e5db4a516a5f7ef6c98d34d716fea1ae239b62a",
4
4
  "mas": "sha256:52c18ffcc940220a3b6aa5985b7e09d24ac27f5ed10a4d660c312e48f73ff105",
5
5
  "livekit": "sha256:6fd3b7088874c4d119160dd688798dfec852bc014786d392caad15f6f63912a3",
package/versions.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "zitadel": "v4.17.3",
2
+ "zitadel": "v4.17.3-auth.4",
3
3
  "synapse": "v1.161.0",
4
4
  "mas": "1.24.0",
5
5
  "livekit": "v1.13.7",