@wtfalch/foundry 0.75.0 → 0.76.1

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.
@@ -13,6 +13,17 @@ export declare const DESKTOP_AUTH_HOME = "desktop_auth";
13
13
  export declare const DESKTOP_AUTH_REVISION_SCOPE_KEY = "coolify_desktop_auth_revision";
14
14
  /** The Agora origin the app was last written for (CLAUDE.md rule 3: the applied hostname is a scope id). */
15
15
  export declare const DESKTOP_AUTH_ORIGIN_SCOPE_KEY = "coolify_desktop_auth_origin";
16
+ /**
17
+ * The org's one shared Postgres server ({@link ORG_POSTGRES_SERVER_SCOPE_KEY}) that desktop-auth's
18
+ * `MESSAGING_DATABASE_URL`/`MESSAGING_DATABASE_URL_OWNER` were last written for. Those two env values
19
+ * are built from the server's live `internal_db_url` at apply time (`executor.ts`'s `serverHome`), so a
20
+ * server that is re-provisioned under a new Coolify resource is always picked up correctly *once this
21
+ * step actually runs* -- but {@link DESKTOP_AUTH_REVISION_SCOPE_KEY} hashes only `desktopAuthEnv`'s
22
+ * fields, which never include the database host. Without this scope key a server change that leaves
23
+ * every other input (issuer, client id, org id, project ids) unchanged never replans the step at all, and
24
+ * the app keeps restarting against a host that no longer exists (foundry#1112).
25
+ */
26
+ export declare const DESKTOP_AUTH_DB_SERVER_SCOPE_KEY = "coolify_desktop_auth_db_server";
16
27
  /** The callback URI last put on Agora's client, or `unsupported:<uri>` when the providers client cannot. */
17
28
  export declare const DESKTOP_AUTH_REDIRECT_SCOPE_KEY = "coolify_desktop_auth_redirect";
18
29
  /** Recorded once the readiness probe through Agora answered. */
@@ -43,10 +54,12 @@ export declare function desktopAuthEnv(inputs: DesktopAuthInputs, runtimeRole: s
43
54
  export declare function desktopAuthRevision(env: Readonly<Record<string, string>>): string;
44
55
  /**
45
56
  * The `coolify:desktop-auth` step. Planned when the app is not recorded, when the env it was written for
46
- * differs (a rename moves the origin), when it never ran healthy, when the callback URI is not on Agora's
47
- * client, and when Agora carries the upstream but the readiness probe never answered. Not planned while
48
- * Agora is blocked (its own line says why) or while the `desktop_auth` home cannot exist yet (blocked).
49
- * `homePlanned` is true when the databases step in this plan creates the home.
57
+ * differs (a rename moves the origin), when the org's Postgres server has changed since the env was last
58
+ * written (foundry#1112: the database host is not part of that revision, since it is built from a live
59
+ * Coolify read the plan itself cannot make), when it never ran healthy, when the callback URI is not on
60
+ * Agora's client, and when Agora carries the upstream but the readiness probe never answered. Not planned
61
+ * while Agora is blocked (its own line says why) or while the `desktop_auth` home cannot exist yet
62
+ * (blocked). `homePlanned` is true when the databases step in this plan creates the home.
50
63
  */
51
64
  export declare function planDesktopAuth(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, input: {
52
65
  host: AgoraHostValues | null;
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { ORG_SERVICE_SCHEMAS, orgServiceDatabase } from '../org.js';
2
+ import { ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, orgServiceDatabase, } from '../org.js';
3
3
  import { AGORA_CLIENT_ID_SCOPE_KEY, DESKTOP_AUTH_APP_SCOPE_KEY, DESKTOP_AUTH_DEPLOYED_SCOPE_KEY, DESKTOP_AUTH_PORT, ZITADEL_ORG_SCOPE_KEY, agoraHostnameFor, desktopAuthInternalName, desktopAuthUpstream, withRecordedMessagingProject, } from './agora.js';
4
4
  import { ZITADEL_FILES_PROJECT_SCOPE_KEY } from './core-files.js';
5
5
  export { DESKTOP_AUTH_APP_SCOPE_KEY, DESKTOP_AUTH_DEPLOYED_SCOPE_KEY, DESKTOP_AUTH_PORT, desktopAuthInternalName, desktopAuthUpstream, };
@@ -14,6 +14,17 @@ export const DESKTOP_AUTH_HOME = 'desktop_auth';
14
14
  export const DESKTOP_AUTH_REVISION_SCOPE_KEY = 'coolify_desktop_auth_revision';
15
15
  /** The Agora origin the app was last written for (CLAUDE.md rule 3: the applied hostname is a scope id). */
16
16
  export const DESKTOP_AUTH_ORIGIN_SCOPE_KEY = 'coolify_desktop_auth_origin';
17
+ /**
18
+ * The org's one shared Postgres server ({@link ORG_POSTGRES_SERVER_SCOPE_KEY}) that desktop-auth's
19
+ * `MESSAGING_DATABASE_URL`/`MESSAGING_DATABASE_URL_OWNER` were last written for. Those two env values
20
+ * are built from the server's live `internal_db_url` at apply time (`executor.ts`'s `serverHome`), so a
21
+ * server that is re-provisioned under a new Coolify resource is always picked up correctly *once this
22
+ * step actually runs* -- but {@link DESKTOP_AUTH_REVISION_SCOPE_KEY} hashes only `desktopAuthEnv`'s
23
+ * fields, which never include the database host. Without this scope key a server change that leaves
24
+ * every other input (issuer, client id, org id, project ids) unchanged never replans the step at all, and
25
+ * the app keeps restarting against a host that no longer exists (foundry#1112).
26
+ */
27
+ export const DESKTOP_AUTH_DB_SERVER_SCOPE_KEY = 'coolify_desktop_auth_db_server';
17
28
  /** The callback URI last put on Agora's client, or `unsupported:<uri>` when the providers client cannot. */
18
29
  export const DESKTOP_AUTH_REDIRECT_SCOPE_KEY = 'coolify_desktop_auth_redirect';
19
30
  /** Recorded once the readiness probe through Agora answered. */
@@ -85,10 +96,12 @@ function runtimeRoleFor(org) {
85
96
  }
86
97
  /**
87
98
  * The `coolify:desktop-auth` step. Planned when the app is not recorded, when the env it was written for
88
- * differs (a rename moves the origin), when it never ran healthy, when the callback URI is not on Agora's
89
- * client, and when Agora carries the upstream but the readiness probe never answered. Not planned while
90
- * Agora is blocked (its own line says why) or while the `desktop_auth` home cannot exist yet (blocked).
91
- * `homePlanned` is true when the databases step in this plan creates the home.
99
+ * differs (a rename moves the origin), when the org's Postgres server has changed since the env was last
100
+ * written (foundry#1112: the database host is not part of that revision, since it is built from a live
101
+ * Coolify read the plan itself cannot make), when it never ran healthy, when the callback URI is not on
102
+ * Agora's client, and when Agora carries the upstream but the readiness probe never answered. Not planned
103
+ * while Agora is blocked (its own line says why) or while the `desktop_auth` home cannot exist yet
104
+ * (blocked). `homePlanned` is true when the databases step in this plan creates the home.
92
105
  */
93
106
  export function planDesktopAuth(org, recorded, input) {
94
107
  if (!input.host)
@@ -114,6 +127,10 @@ export function planDesktopAuth(org, recorded, input) {
114
127
  else {
115
128
  if (revision && recorded[DESKTOP_AUTH_REVISION_SCOPE_KEY] !== revision)
116
129
  reasons.push('env');
130
+ const dbServer = recorded[ORG_POSTGRES_SERVER_SCOPE_KEY];
131
+ if (dbServer && recorded[DESKTOP_AUTH_DB_SERVER_SCOPE_KEY] !== dbServer) {
132
+ reasons.push('database host changed');
133
+ }
117
134
  if (!recorded[DESKTOP_AUTH_DEPLOYED_SCOPE_KEY])
118
135
  reasons.push('never ran healthy');
119
136
  if (redirect !== callback && !blocked)
@@ -32,7 +32,7 @@ import { LOGIN_COOKIE_DOMAIN_SCOPE_KEY } from './cookie-domain.js';
32
32
  import { previewsOn } from './coolify-previews.js';
33
33
  import { CORE_FILES_COLLECTION_NAME, FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY, } from './core-files.js';
34
34
  import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
35
- import { DESKTOP_AUTH_APP_SCOPE_KEY, DESKTOP_AUTH_DEPLOYED_SCOPE_KEY, DESKTOP_AUTH_DOCKERFILE, DESKTOP_AUTH_HOME, DESKTOP_AUTH_ORIGIN_SCOPE_KEY, DESKTOP_AUTH_READY_PATH, DESKTOP_AUTH_READY_SCOPE_KEY, DESKTOP_AUTH_REDIRECT_SCOPE_KEY, DESKTOP_AUTH_REVISION_SCOPE_KEY, desktopAuthCallbackUri, desktopAuthEnv, desktopAuthInputs, desktopAuthRedirectUnsupported, desktopAuthRevision, } from './desktop-auth.js';
35
+ import { DESKTOP_AUTH_APP_SCOPE_KEY, DESKTOP_AUTH_DB_SERVER_SCOPE_KEY, DESKTOP_AUTH_DEPLOYED_SCOPE_KEY, DESKTOP_AUTH_DOCKERFILE, DESKTOP_AUTH_HOME, DESKTOP_AUTH_ORIGIN_SCOPE_KEY, DESKTOP_AUTH_READY_PATH, DESKTOP_AUTH_READY_SCOPE_KEY, DESKTOP_AUTH_REDIRECT_SCOPE_KEY, DESKTOP_AUTH_REVISION_SCOPE_KEY, desktopAuthCallbackUri, desktopAuthEnv, desktopAuthInputs, desktopAuthRedirectUnsupported, desktopAuthRevision, } from './desktop-auth.js';
36
36
  import { desktopCallbackUri, signInCallbackAppliedValue, signInRedirectUris, unionRedirectUris, zitadelAppDesktopCallbackAppliedScopeKey, } from './desktop-callback.js';
37
37
  import { PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, onPlatformNetwork, placed, placedByScope, placedOnPlatform, } from './destinations.js';
38
38
  import { DEFAULT_AI_LIMIT_MICROS } from './diff.js';
@@ -7747,6 +7747,12 @@ export class FoundryStepExecutor {
7747
7747
  await this.addDesktopAuthCallback(org, context, desktopAuthCallbackUri(inputs.origin));
7748
7748
  context.recordScopeId(DESKTOP_AUTH_ORIGIN_SCOPE_KEY, inputs.origin);
7749
7749
  context.recordScopeId(DESKTOP_AUTH_REVISION_SCOPE_KEY, desktopAuthRevision(env));
7750
+ // foundry#1112: the database host itself is not part of `env`'s revision (it comes from a live
7751
+ // Coolify read, not from `desktopAuthEnv`'s inputs), so the server this env was built against is
7752
+ // tracked here instead -- a later change replans this step even when nothing else drifted.
7753
+ const dbServerId = recorded[ORG_POSTGRES_SERVER_SCOPE_KEY];
7754
+ if (dbServerId)
7755
+ context.recordScopeId(DESKTOP_AUTH_DB_SERVER_SCOPE_KEY, dbServerId);
7750
7756
  return { resourceId: app.appId };
7751
7757
  }
7752
7758
  /** Whether the org's running Agora carries `AGORA_DESKTOP_AUTH_UPSTREAM`, read from its live env. */
@@ -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_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_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)) &&
@@ -86,6 +86,16 @@ export declare const AUTH_LOGIN_ENV_VERSION = "4";
86
86
  export declare function authLoginEnvMarker(smtpSender: string, passkeyRpId?: string): string;
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
+ /**
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";
89
99
  /**
90
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
@@ -95,6 +95,16 @@ export function authLoginEnvMarker(smtpSender, passkeyRpId) {
95
95
  }
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
+ /**
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';
98
108
  /**
99
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
@@ -2300,6 +2310,8 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
2300
2310
  // step's redeploy, so a platform recorded before them still plans it.
2301
2311
  recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
2302
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 &&
2303
2315
  // The login apps' mail env: a platform from before it, or with a new apex, is written again.
2304
2316
  // Without the held key the apply cannot record it, so it is asked for only once the key is held.
2305
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_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_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';
@@ -1504,8 +1504,8 @@ export const PLATFORM_AUTH_PROJECT = 'platform';
1504
1504
  * header and so cannot give two different apps the same public hostname.
1505
1505
  */
1506
1506
  export const PLATFORM_AUTH_LOGIN_INTERNAL_NAME = 'platform-auth-login';
1507
- /** `wtfalch/auth`'s `docker-compose.yml`: `image: ghcr.io/zitadel/zitadel:${ZITADEL_VERSION:-v4.17.3}`. Pinned in `versions.json` (#247). */
1508
- 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';
1509
1509
  const AUTH_ZITADEL_IMAGE_TAG = VERSIONS.zitadel;
1510
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". */
1511
1511
  const AUTH_REPO = 'wtfalch/auth';
@@ -2264,6 +2264,36 @@ export async function provisionAuthStack(scope, deps, target, input) {
2264
2264
  loginEnvs.push({ key: 'LOGIN_MAIL_FROM', value: input.smtpSender });
2265
2265
  if (input.passkeyRpId)
2266
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
+ }
2267
2297
  // `AUTH_APPS` and `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` are not pushed here
2268
2298
  // -- this function's own doc comment, "Known gaps". `LETTERMINT_API_TOKEN`
2269
2299
  // is, once held (#210): pushing it costs nothing even while the broker
@@ -2300,7 +2330,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
2300
2330
  // container on its next deploy (`diff.ts`'s `coolifyCalls` follows the
2301
2331
  // identical rule).
2302
2332
  const zitadelEnvsChanged = await envsDiffer(deps.coolify, zitadelAppId, zitadelEnvs);
2303
- const loginEnvsChanged = await envsDiffer(deps.coolify, loginAppId, loginEnvs);
2333
+ const loginEnvsChanged = await envsDiffer(deps.coolify, loginAppId, withoutIssuerSourceEnvs(loginEnvs));
2304
2334
  const brokerEnvsChanged = await envsDiffer(deps.coolify, brokerAppId, brokerEnvs);
2305
2335
  await deps.coolify.bulkUpsertEnvs(zitadelAppId, zitadelEnvs);
2306
2336
  await deps.coolify.bulkUpsertEnvs(loginAppId, loginEnvs);
@@ -2332,7 +2362,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
2332
2362
  { key: SIGN_IN_BIND_KEY_ENV, value: bindKey },
2333
2363
  ...(bindings ? [{ key: LOGIN_HOST_BINDINGS_ENV, value: bindings }] : []),
2334
2364
  ];
2335
- signInEnvsChanged = await envsDiffer(deps.coolify, signInApp.appId, signInEnvs);
2365
+ signInEnvsChanged = await envsDiffer(deps.coolify, signInApp.appId, withoutIssuerSourceEnvs(signInEnvs));
2336
2366
  await deps.coolify.bulkUpsertEnvs(signInApp.appId, signInEnvs);
2337
2367
  staleRpIdSignIn = input.passkeyRpId
2338
2368
  ? false
@@ -2388,6 +2418,11 @@ export async function provisionAuthStack(scope, deps, target, input) {
2388
2418
  if (input.signInLogin)
2389
2419
  await record(SIGN_IN_BIND_KEY_SCOPE_KEY, '1');
2390
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
+ }
2391
2426
  // The mail env is live on the login apps: written above and deployed with them. Without the held
2392
2427
  // key the marker stays unset, so the step is planned again once the key is held.
2393
2428
  if (heldMailKey && input.smtpSender) {
@@ -7653,6 +7688,33 @@ export function brokerDeployRecordValue(brokerAppId, authAppsRaw) {
7653
7688
  return JSON.stringify(record);
7654
7689
  }
7655
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
+ }
7656
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. */
7657
7719
  async function removeLoginEnv(coolify, appId, key) {
7658
7720
  if (typeof coolify.deleteEnv !== 'function')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.75.0",
3
+ "version": "0.76.1",
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",