@wtfalch/foundry 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/org.d.ts +26 -1
- package/dist/org.js +56 -0
- package/dist/plan/boule.d.ts +7 -1
- package/dist/plan/boule.js +16 -0
- package/dist/plan/broker-entries.js +11 -2
- package/dist/plan/broker-mcp.d.ts +63 -0
- package/dist/plan/broker-mcp.js +158 -0
- package/dist/plan/diff.d.ts +12 -0
- package/dist/plan/diff.js +34 -10
- package/dist/plan/executor.d.ts +7 -0
- package/dist/plan/executor.js +64 -7
- package/dist/plan/login-host.d.ts +8 -0
- package/dist/plan/login-host.js +14 -0
- package/dist/plan/own-diff.js +4 -1
- package/dist/plan/platform-diff.d.ts +16 -0
- package/dist/plan/platform-diff.js +19 -0
- package/dist/plan/platform-providers.d.ts +42 -0
- package/dist/plan/platform-providers.js +72 -8
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, BlueGreenConfig, OrgBuiltinApp, OrgRefusal, OrgRefusalCode, WorkerAppConfig, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceDatabase, OrgServiceSchema, ServicesConfig, SignInConfig, VersionPin, VersionPinPart, } from './org.js';
|
|
1
|
+
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, BlueGreenConfig, OrgBuiltinApp, OrgRefusal, OrgRefusalCode, WorkerAppConfig, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryAuthRoom, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceDatabase, OrgServiceSchema, ServicesConfig, SignInConfig, VersionPin, VersionPinPart, } from './org.js';
|
|
2
2
|
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, orgServiceDatabase, orgServiceDatabases, SFU_SLOT_MAX, VERSION_PIN_PARTS, isMainEntry, mainEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, normalizeHostingRepos, validateMail, validateOwners, validateServices, validateCustomers, validateSignIn, validateVersionPin, validateSite, } from './org.js';
|
|
3
3
|
export { refuseOrg } from './refuse-org.js';
|
|
4
4
|
export { workerAppHasBuild } from './worker-app-config.js';
|
package/dist/org.d.ts
CHANGED
|
@@ -289,7 +289,7 @@ export type AccessRule = {
|
|
|
289
289
|
/** The key of the entry that is today's one app: the Coolify app named `<slug>`. */
|
|
290
290
|
export declare const APP_ENTRY_KEY = "app";
|
|
291
291
|
/** Why a validator refused. `code` is stable and never reworded; `message` is for people. */
|
|
292
|
-
export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'client_name_taken' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
|
|
292
|
+
export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'auth_room_invalid' | 'auth_room_needs_customers' | 'auth_room_not_unique' | 'client_name_taken' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
|
|
293
293
|
/**
|
|
294
294
|
* One refusal: a stable `code`, the dotted `path` into {@link DeclaredOrg} it is about
|
|
295
295
|
* (`hosting.entries.terminal.uses.auth.registration`) and the sentence for people.
|
|
@@ -498,7 +498,32 @@ export interface EntryAuth {
|
|
|
498
498
|
* `users: 'customers'`.
|
|
499
499
|
*/
|
|
500
500
|
signIn?: 'password' | 'email-code';
|
|
501
|
+
/**
|
|
502
|
+
* The left-half text of the customers' sign-in window. Written on the
|
|
503
|
+
* customers organisation as metadata `wtfalch.room` (JSON, keys in the
|
|
504
|
+
* order below). Needs `users: 'customers'`; at most one entry per org
|
|
505
|
+
* declares it.
|
|
506
|
+
*/
|
|
507
|
+
room?: EntryAuthRoom;
|
|
501
508
|
}
|
|
509
|
+
export interface EntryAuthRoom {
|
|
510
|
+
name: string;
|
|
511
|
+
headline: string;
|
|
512
|
+
line: string;
|
|
513
|
+
title?: string;
|
|
514
|
+
description?: string;
|
|
515
|
+
steps?: number;
|
|
516
|
+
}
|
|
517
|
+
/** Longest value of each `uses.auth.room` text field. */
|
|
518
|
+
export declare const ROOM_TEXT_MAX: {
|
|
519
|
+
readonly name: 40;
|
|
520
|
+
readonly headline: 120;
|
|
521
|
+
readonly line: 200;
|
|
522
|
+
readonly title: 60;
|
|
523
|
+
readonly description: 200;
|
|
524
|
+
};
|
|
525
|
+
export declare const ROOM_STEPS_MIN = 2;
|
|
526
|
+
export declare const ROOM_STEPS_MAX = 9;
|
|
502
527
|
/** `uses.auth.mcp.onboardingPath`: absolute, no query, no fragment, no dot segments. */
|
|
503
528
|
export declare const ONBOARDING_PATH_MAX_LENGTH = 200;
|
|
504
529
|
/** The MCP endpoint path: absolute, plain characters, no dot segments, no query. */
|
package/dist/org.js
CHANGED
|
@@ -15,6 +15,16 @@ function refusal(code, path, message) {
|
|
|
15
15
|
}
|
|
16
16
|
/** The most volumes one entry may declare. */
|
|
17
17
|
export const MAX_ENTRY_VOLUMES = 8;
|
|
18
|
+
/** Longest value of each `uses.auth.room` text field. */
|
|
19
|
+
export const ROOM_TEXT_MAX = {
|
|
20
|
+
name: 40,
|
|
21
|
+
headline: 120,
|
|
22
|
+
line: 200,
|
|
23
|
+
title: 60,
|
|
24
|
+
description: 200,
|
|
25
|
+
};
|
|
26
|
+
export const ROOM_STEPS_MIN = 2;
|
|
27
|
+
export const ROOM_STEPS_MAX = 9;
|
|
18
28
|
/** `uses.auth.mcp.onboardingPath`: absolute, no query, no fragment, no dot segments. */
|
|
19
29
|
export const ONBOARDING_PATH_MAX_LENGTH = 200;
|
|
20
30
|
/** The MCP endpoint path: absolute, plain characters, no dot segments, no query. */
|
|
@@ -216,6 +226,10 @@ export function refuseHosting(hosting, names) {
|
|
|
216
226
|
if (mains.length > 1) {
|
|
217
227
|
return refusal('hosting_main_count', 'hosting.entries', `at most one hosting entry may be the main app: ${mains.map((entry) => `'${entry.key}'`).join(' and ')} are`);
|
|
218
228
|
}
|
|
229
|
+
const roomEntries = hosting.entries.filter((entry) => entry.uses?.auth?.room !== undefined);
|
|
230
|
+
if (roomEntries.length > 1) {
|
|
231
|
+
return refusal('auth_room_not_unique', 'hosting.entries', `at most one hosting entry may set uses.auth.room: ${roomEntries.map((entry) => `'${entry.key}'`).join(' and ')} do`);
|
|
232
|
+
}
|
|
219
233
|
if (hosting.entries.filter((entry) => entry.uses?.ai?.webhookPath !== undefined).length > 1) {
|
|
220
234
|
return refusal('ai_webhook_path_not_unique', 'hosting.entries', 'at most one hosting entry may set uses.ai webhookPath: ai keeps one webhook per organisation');
|
|
221
235
|
}
|
|
@@ -649,6 +663,15 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
649
663
|
if (signIn === 'email-code' && auth.users !== 'customers') {
|
|
650
664
|
return refusal('email_code_needs_customers', `hosting.entries.${entry.key}.uses.auth.signIn`, `${at} signIn 'email-code' needs users 'customers': only customers sign in with an emailed code`);
|
|
651
665
|
}
|
|
666
|
+
if (auth.room !== undefined) {
|
|
667
|
+
if (auth.users !== 'customers') {
|
|
668
|
+
return refusal('auth_room_needs_customers', `hosting.entries.${entry.key}.uses.auth.room`, `${at} room needs users 'customers': only the customers' sign-in window shows it`);
|
|
669
|
+
}
|
|
670
|
+
const problem = roomProblem(auth.room);
|
|
671
|
+
if (problem) {
|
|
672
|
+
return refusal('auth_room_invalid', `hosting.entries.${entry.key}.uses.auth.room`, `${at} room ${problem}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
652
675
|
if (entry.key === 'agora' ||
|
|
653
676
|
entry.key === 'boule' ||
|
|
654
677
|
entry.key === 'cms' ||
|
|
@@ -657,6 +680,39 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
657
680
|
}
|
|
658
681
|
return null;
|
|
659
682
|
}
|
|
683
|
+
const ROOM_KEYS = [...Object.keys(ROOM_TEXT_MAX), 'steps'];
|
|
684
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: the point is to refuse them
|
|
685
|
+
const ROOM_UNSAFE_TEXT = /[\u0000-\u001f\u007f<>]/;
|
|
686
|
+
/** A fixed sentence when `uses.auth.room` is not usable, `null` when it is. The value is never in it. */
|
|
687
|
+
function roomProblem(room) {
|
|
688
|
+
if (typeof room !== 'object' || room === null || Array.isArray(room)) {
|
|
689
|
+
return 'must be an object with name, headline and line';
|
|
690
|
+
}
|
|
691
|
+
const fields = room;
|
|
692
|
+
const unknown = Object.keys(fields).find((key) => !ROOM_KEYS.includes(key));
|
|
693
|
+
if (unknown !== undefined)
|
|
694
|
+
return `has an unknown key '${unknown}'`;
|
|
695
|
+
for (const [key, max] of Object.entries(ROOM_TEXT_MAX)) {
|
|
696
|
+
const value = fields[key];
|
|
697
|
+
if (value === undefined && (key === 'title' || key === 'description'))
|
|
698
|
+
continue;
|
|
699
|
+
if (typeof value !== 'string' ||
|
|
700
|
+
value.trim() === '' ||
|
|
701
|
+
value.length > max ||
|
|
702
|
+
ROOM_UNSAFE_TEXT.test(value)) {
|
|
703
|
+
return `${key} must be 1 to ${max} characters of plain text with no control character, '<' or '>'`;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const steps = fields.steps;
|
|
707
|
+
if (steps !== undefined &&
|
|
708
|
+
(typeof steps !== 'number' ||
|
|
709
|
+
!Number.isInteger(steps) ||
|
|
710
|
+
steps < ROOM_STEPS_MIN ||
|
|
711
|
+
steps > ROOM_STEPS_MAX)) {
|
|
712
|
+
return `steps must be a whole number from ${ROOM_STEPS_MIN} to ${ROOM_STEPS_MAX}`;
|
|
713
|
+
}
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
660
716
|
/** With no domain the hostnames are not read (`entryHostnames`). */
|
|
661
717
|
function hasExactHostname(entry, hosting, domain) {
|
|
662
718
|
if (entry.workerApp)
|
package/dist/plan/boule.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DeclaredOrg, type OwnerIdentity } from '../org.js';
|
|
1
|
+
import { type DeclaredOrg, type EntryAuthRoom, type OwnerIdentity } from '../org.js';
|
|
2
2
|
import type { BrokerEntryInput, BrokerEntryWant } from './broker-entries.js';
|
|
3
3
|
/**
|
|
4
4
|
* #161 (Boule part): what both the planner (`diff.ts`) and the executor
|
|
@@ -106,6 +106,12 @@ export declare const CUSTOMERS_DIRECTORY_USER_SCOPE_KEY = "customers_directory_u
|
|
|
106
106
|
export declare const CUSTOMERS_SIGN_IN_METADATA_KEY = "wtfalch.signIn";
|
|
107
107
|
/** `email-code` or `password`: the sign-in metadata last written to the customers organisation. Absent: never written. */
|
|
108
108
|
export declare const CUSTOMERS_SIGN_IN_SCOPE_KEY = "zitadel_customers_signin";
|
|
109
|
+
/** The ZITADEL org metadata key that holds the left-half text of the customers' sign-in window. */
|
|
110
|
+
export declare const CUSTOMERS_ROOM_METADATA_KEY = "wtfalch.room";
|
|
111
|
+
/** The metadata value last written to the customers organisation, or empty after a removal. Absent: never written. */
|
|
112
|
+
export declare const CUSTOMERS_ROOM_SCOPE_KEY = "zitadel_customers_room";
|
|
113
|
+
/** The `wtfalch.room` value: JSON, keys in the order name, headline, line, title, description, steps. */
|
|
114
|
+
export declare function customersRoomValue(room: EntryAuthRoom): string;
|
|
109
115
|
/** `1` or `0`: the sign-up switch last written to the customers organisation's login policy. */
|
|
110
116
|
export declare const CUSTOMERS_SIGN_UP_SCOPE_KEY = "zitadel_customers_signup";
|
|
111
117
|
/** Where the held-key store keeps the Archon credential of an org's Boule. */
|
package/dist/plan/boule.js
CHANGED
|
@@ -120,6 +120,22 @@ export const CUSTOMERS_DIRECTORY_USER_SCOPE_KEY = 'customers_directory_user';
|
|
|
120
120
|
export const CUSTOMERS_SIGN_IN_METADATA_KEY = 'wtfalch.signIn';
|
|
121
121
|
/** `email-code` or `password`: the sign-in metadata last written to the customers organisation. Absent: never written. */
|
|
122
122
|
export const CUSTOMERS_SIGN_IN_SCOPE_KEY = 'zitadel_customers_signin';
|
|
123
|
+
/** The ZITADEL org metadata key that holds the left-half text of the customers' sign-in window. */
|
|
124
|
+
export const CUSTOMERS_ROOM_METADATA_KEY = 'wtfalch.room';
|
|
125
|
+
/** The metadata value last written to the customers organisation, or empty after a removal. Absent: never written. */
|
|
126
|
+
export const CUSTOMERS_ROOM_SCOPE_KEY = 'zitadel_customers_room';
|
|
127
|
+
/** The `wtfalch.room` value: JSON, keys in the order name, headline, line, title, description, steps. */
|
|
128
|
+
export function customersRoomValue(room) {
|
|
129
|
+
const { name, headline, line, title, description, steps } = room;
|
|
130
|
+
return JSON.stringify({
|
|
131
|
+
name,
|
|
132
|
+
headline,
|
|
133
|
+
line,
|
|
134
|
+
...(title !== undefined ? { title } : {}),
|
|
135
|
+
...(description !== undefined ? { description } : {}),
|
|
136
|
+
...(steps !== undefined ? { steps } : {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
123
139
|
/** `1` or `0`: the sign-up switch last written to the customers organisation's login policy. */
|
|
124
140
|
export const CUSTOMERS_SIGN_UP_SCOPE_KEY = 'zitadel_customers_signup';
|
|
125
141
|
/** Where the held-key store keeps the Archon credential of an org's Boule. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isFoundryOwnedBrokerEntryName, } from '../org.js';
|
|
3
3
|
import { entryClientIdScopeKey } from './agora.js';
|
|
4
|
+
import { MCP_DATABASE_URL_ENV, MCP_OAUTH_SECRET_ENV } from './broker-mcp.js';
|
|
4
5
|
import { entryHostnames } from './hostnames.js';
|
|
5
6
|
import { loginHostnameFor } from './login-host.js';
|
|
6
7
|
import { PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, parseBrokerDeployRecord, } from './platform-diff.js';
|
|
@@ -185,7 +186,8 @@ export async function writeBrokerEntry(broker, input) {
|
|
|
185
186
|
if (archonOrgConflicts([want], archonOrgId).length > 0) {
|
|
186
187
|
throw new Error(`${label} would sign in Archon's own organisation, which no other entry may`);
|
|
187
188
|
}
|
|
188
|
-
const
|
|
189
|
+
const brokerEnvs = await broker.listEnvs();
|
|
190
|
+
const env = brokerEnvs.find((e) => e.key === 'AUTH_APPS' && !e.is_preview);
|
|
189
191
|
// A value that reads back empty is not an empty list: writing over it
|
|
190
192
|
// would drop every hand-written entry.
|
|
191
193
|
if (env && !env.value) {
|
|
@@ -275,8 +277,15 @@ export async function writeBrokerEntry(broker, input) {
|
|
|
275
277
|
place();
|
|
276
278
|
const nextRaw = JSON.stringify(next);
|
|
277
279
|
const changed = JSON.stringify(current) !== nextRaw;
|
|
278
|
-
if (changed)
|
|
280
|
+
if (changed) {
|
|
281
|
+
// Auth ADR 0002: a broker given an `mcp` entry without its database and secret exits at boot. The
|
|
282
|
+
// sign-in step sets both first; until it has, nothing is written and the next apply tries again.
|
|
283
|
+
const held = (key) => brokerEnvs.some((e) => e.key === key && !e.is_preview && e.value != null && e.value !== '');
|
|
284
|
+
if (want.mcp && !(held(MCP_DATABASE_URL_ENV) && held(MCP_OAUTH_SECRET_ENV))) {
|
|
285
|
+
throw new Error(`${label}: the broker has no ${MCP_DATABASE_URL_ENV} and ${MCP_OAUTH_SECRET_ENV} yet (the sign-in step sets them), so nothing was written; apply again`);
|
|
286
|
+
}
|
|
279
287
|
await broker.setAuthApps(nextRaw);
|
|
288
|
+
}
|
|
280
289
|
// The platform's own check of the broker's inputs also refuses a broker
|
|
281
290
|
// whose AUTH_APPS holds an invalid entry; the broker itself only drops it,
|
|
282
291
|
// so that one does not hold back the deploy.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CoolifyPort } from './executor.js';
|
|
2
|
+
import { type JobRuntime } from './job-app.js';
|
|
3
|
+
/**
|
|
4
|
+
* Auth ADR 0002, sections 2 and 5: a broker whose `AUTH_APPS` holds an entry with `mcp` exits at boot
|
|
5
|
+
* without `MCP_DATABASE_URL` (a `broker` database on the sign-in Postgres) and `MCP_OAUTH_SECRET`
|
|
6
|
+
* (`deriveKeys` in `wtfalch/auth` broker/src/mcp-crypto.ts wants at least 32 characters). This file
|
|
7
|
+
* gives the broker both, from `provisionAuthStack`, before an entry with `mcp` can reach it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MCP_DATABASE_URL_ENV = "MCP_DATABASE_URL";
|
|
10
|
+
export declare const MCP_OAUTH_SECRET_ENV = "MCP_OAUTH_SECRET";
|
|
11
|
+
/** The broker's own role and database on the sign-in Postgres. */
|
|
12
|
+
export declare const BROKER_DB_NAME = "broker";
|
|
13
|
+
export declare const BROKER_DB_USER = "broker";
|
|
14
|
+
/** 32 random bytes as 64 hex characters: twice the 32 characters `deriveKeys` asks for. */
|
|
15
|
+
export declare const MCP_OAUTH_SECRET_MIN_LENGTH = 32;
|
|
16
|
+
type Coolify = Pick<CoolifyPort, 'listEnvs' | 'bulkUpsertEnvs' | 'findApplicationByName' | 'createDockerfileApplication' | 'updateApplication' | 'deploy' | 'getApplication' | 'listServers' | 'raw'>;
|
|
17
|
+
/**
|
|
18
|
+
* The job's script. Idempotent: a role or database that exists is kept, and the role's password is set
|
|
19
|
+
* to the one given. The password reaches SQL through `\getenv` and `format('%L')`, never through the
|
|
20
|
+
* shell, and psql's output is discarded so no line carries a URL or a password. `ADMIN_DATABASE_URL`
|
|
21
|
+
* is the sign-in database's own DSN; for the TLS database the CA is written to the path its
|
|
22
|
+
* `sslrootcert` names.
|
|
23
|
+
*/
|
|
24
|
+
export declare function brokerDbJobScript(): string[];
|
|
25
|
+
/** The job: the sign-in database's pinned image, so `psql` matches the server's major version. */
|
|
26
|
+
export declare function brokerDbJobDockerfile(): string;
|
|
27
|
+
export interface BrokerMcpInput {
|
|
28
|
+
coolify: Coolify;
|
|
29
|
+
brokerAppId: string;
|
|
30
|
+
/** The job app's Coolify name. */
|
|
31
|
+
jobName: string;
|
|
32
|
+
project: {
|
|
33
|
+
uuid: string;
|
|
34
|
+
environment: {
|
|
35
|
+
id: number;
|
|
36
|
+
name: string;
|
|
37
|
+
uuid: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** The sign-in database's own (superuser) DSN. A secret. */
|
|
41
|
+
adminUrl: string;
|
|
42
|
+
/** The CA PEM when the database is the TLS image; public. */
|
|
43
|
+
caPem: string | undefined;
|
|
44
|
+
/** The broker role's connection string for `password`. A secret. */
|
|
45
|
+
brokerUrl(password: string): string;
|
|
46
|
+
runtime: JobRuntime & {
|
|
47
|
+
onSecret?: (value: string) => void;
|
|
48
|
+
};
|
|
49
|
+
label: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The two env entries a broker with `mcp` entries needs, for the caller to write with the rest of the
|
|
53
|
+
* broker's env. A live value of either is kept (a hand-set one too), so a rerun makes nothing and the
|
|
54
|
+
* `broker` database job runs only while `MCP_DATABASE_URL` is unset. The secret is minted with
|
|
55
|
+
* `ensureGeneratedEnv`. The role's password is minted only when the URL is unset; a run that fails after
|
|
56
|
+
* the job and before the URL is written mints another and the job sets it, since nothing else uses the
|
|
57
|
+
* old one. Secrets go only to the job's and the broker's env and to `runtime.onSecret`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ensureBrokerMcpEnv(input: BrokerMcpInput): Promise<Array<{
|
|
60
|
+
key: string;
|
|
61
|
+
value: string;
|
|
62
|
+
}>>;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { pinnedRef } from '../bootstrap/versions.js';
|
|
3
|
+
import { JobFailure, waitForJob } from './job-app.js';
|
|
4
|
+
import { currentEnvValue, ensureGeneratedEnv, resolvePlatformServer, } from './platform-providers.js';
|
|
5
|
+
import { SIGNIN_DB_CA_MOUNT, boundedCall } from './signin-db.js';
|
|
6
|
+
/**
|
|
7
|
+
* Auth ADR 0002, sections 2 and 5: a broker whose `AUTH_APPS` holds an entry with `mcp` exits at boot
|
|
8
|
+
* without `MCP_DATABASE_URL` (a `broker` database on the sign-in Postgres) and `MCP_OAUTH_SECRET`
|
|
9
|
+
* (`deriveKeys` in `wtfalch/auth` broker/src/mcp-crypto.ts wants at least 32 characters). This file
|
|
10
|
+
* gives the broker both, from `provisionAuthStack`, before an entry with `mcp` can reach it.
|
|
11
|
+
*/
|
|
12
|
+
export const MCP_DATABASE_URL_ENV = 'MCP_DATABASE_URL';
|
|
13
|
+
export const MCP_OAUTH_SECRET_ENV = 'MCP_OAUTH_SECRET';
|
|
14
|
+
/** The broker's own role and database on the sign-in Postgres. */
|
|
15
|
+
export const BROKER_DB_NAME = 'broker';
|
|
16
|
+
export const BROKER_DB_USER = 'broker';
|
|
17
|
+
/** 32 random bytes as 64 hex characters: twice the 32 characters `deriveKeys` asks for. */
|
|
18
|
+
export const MCP_OAUTH_SECRET_MIN_LENGTH = 32;
|
|
19
|
+
const JOB_ADMIN_ENV = 'ADMIN_DATABASE_URL';
|
|
20
|
+
const JOB_PASSWORD_ENV = 'BROKER_DB_PASSWORD';
|
|
21
|
+
const JOB_CA_ENV = 'SIGNIN_DB_CA_B64';
|
|
22
|
+
const JOB_DONE_FILE = '/tmp/done';
|
|
23
|
+
const JOB_FAILED_FILE = '/tmp/failed';
|
|
24
|
+
/**
|
|
25
|
+
* The job's script. Idempotent: a role or database that exists is kept, and the role's password is set
|
|
26
|
+
* to the one given. The password reaches SQL through `\getenv` and `format('%L')`, never through the
|
|
27
|
+
* shell, and psql's output is discarded so no line carries a URL or a password. `ADMIN_DATABASE_URL`
|
|
28
|
+
* is the sign-in database's own DSN; for the TLS database the CA is written to the path its
|
|
29
|
+
* `sslrootcert` names.
|
|
30
|
+
*/
|
|
31
|
+
export function brokerDbJobScript() {
|
|
32
|
+
return [
|
|
33
|
+
'#!/bin/sh',
|
|
34
|
+
'set -eu',
|
|
35
|
+
`trap '[ -f ${JOB_DONE_FILE} ] || touch ${JOB_FAILED_FILE}' EXIT`,
|
|
36
|
+
"trap 'exit 1' HUP INT TERM",
|
|
37
|
+
`[ ! -f ${JOB_FAILED_FILE} ] || exit 1`,
|
|
38
|
+
`rm -f ${JOB_DONE_FILE}`,
|
|
39
|
+
'fail() { echo "broker-db: failed"; exit 1; }',
|
|
40
|
+
`[ -n "\${${JOB_PASSWORD_ENV}:-}" ] || fail`,
|
|
41
|
+
`if [ -n "\${${JOB_CA_ENV}:-}" ]; then mkdir -p "$(dirname ${SIGNIN_DB_CA_MOUNT})"; printf %s "$${JOB_CA_ENV}" | base64 -d > ${SIGNIN_DB_CA_MOUNT}; fi`,
|
|
42
|
+
`psql "$${JOB_ADMIN_ENV}" -X -q -v ON_ERROR_STOP=1 > /dev/null 2>&1 <<'SQL' || fail`,
|
|
43
|
+
`\\getenv pw ${JOB_PASSWORD_ENV}`,
|
|
44
|
+
`SELECT 'CREATE ROLE ${BROKER_DB_USER} LOGIN' WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${BROKER_DB_USER}') \\gexec`,
|
|
45
|
+
`SELECT format('ALTER ROLE ${BROKER_DB_USER} PASSWORD %L', :'pw') \\gexec`,
|
|
46
|
+
`SELECT 'CREATE DATABASE ${BROKER_DB_NAME} OWNER ${BROKER_DB_USER} TEMPLATE template0 ENCODING ''UTF8''' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${BROKER_DB_NAME}') \\gexec`,
|
|
47
|
+
'SQL',
|
|
48
|
+
'echo "broker-db: ready"',
|
|
49
|
+
`touch ${JOB_DONE_FILE}`,
|
|
50
|
+
'exec sleep infinity',
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
/** The job: the sign-in database's pinned image, so `psql` matches the server's major version. */
|
|
54
|
+
export function brokerDbJobDockerfile() {
|
|
55
|
+
const encoded = Buffer.from(`${brokerDbJobScript().join('\n')}\n`, 'utf8').toString('base64');
|
|
56
|
+
return [
|
|
57
|
+
`FROM postgres:${pinnedRef('postgres')}`,
|
|
58
|
+
`RUN echo '${encoded}' | base64 -d > /usr/local/bin/broker-db && chmod 0755 /usr/local/bin/broker-db`,
|
|
59
|
+
'ENTRYPOINT ["/usr/local/bin/broker-db"]',
|
|
60
|
+
`HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=1 CMD ["test", "-f", "${JOB_DONE_FILE}"]`,
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The two env entries a broker with `mcp` entries needs, for the caller to write with the rest of the
|
|
65
|
+
* broker's env. A live value of either is kept (a hand-set one too), so a rerun makes nothing and the
|
|
66
|
+
* `broker` database job runs only while `MCP_DATABASE_URL` is unset. The secret is minted with
|
|
67
|
+
* `ensureGeneratedEnv`. The role's password is minted only when the URL is unset; a run that fails after
|
|
68
|
+
* the job and before the URL is written mints another and the job sets it, since nothing else uses the
|
|
69
|
+
* old one. Secrets go only to the job's and the broker's env and to `runtime.onSecret`.
|
|
70
|
+
*/
|
|
71
|
+
export async function ensureBrokerMcpEnv(input) {
|
|
72
|
+
const { coolify, brokerAppId, runtime } = input;
|
|
73
|
+
let url = await currentEnvValue(coolify, brokerAppId, MCP_DATABASE_URL_ENV);
|
|
74
|
+
if (!url) {
|
|
75
|
+
const password = randomBytes(24).toString('base64url');
|
|
76
|
+
runtime.onSecret?.(password);
|
|
77
|
+
await runBrokerDbJob(input, password);
|
|
78
|
+
url = input.brokerUrl(password);
|
|
79
|
+
}
|
|
80
|
+
runtime.onSecret?.(url);
|
|
81
|
+
const minted = await ensureGeneratedEnv(coolify, brokerAppId, MCP_OAUTH_SECRET_ENV, () => randomBytes(32).toString('hex'), MCP_OAUTH_SECRET_MIN_LENGTH);
|
|
82
|
+
const secret = minted ?? (await currentEnvValue(coolify, brokerAppId, MCP_OAUTH_SECRET_ENV));
|
|
83
|
+
if (!secret) {
|
|
84
|
+
throw new Error(`${input.label}: the broker lost its ${MCP_OAUTH_SECRET_ENV} between two reads`);
|
|
85
|
+
}
|
|
86
|
+
runtime.onSecret?.(secret);
|
|
87
|
+
return [
|
|
88
|
+
{ key: MCP_DATABASE_URL_ENV, value: url },
|
|
89
|
+
{ key: MCP_OAUTH_SECRET_ENV, value: secret },
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
/** Runs the job once, waits for it, deletes it. Every error is fixed text with the app id, never a cause. */
|
|
93
|
+
async function runBrokerDbJob(input, password) {
|
|
94
|
+
const { label, runtime } = input;
|
|
95
|
+
const coolify = input.coolify;
|
|
96
|
+
const { uuid: projectUuid, environment } = input.project;
|
|
97
|
+
let jobAppId;
|
|
98
|
+
try {
|
|
99
|
+
// A job left by an earlier run holds an old script and password: it is replaced, never reused.
|
|
100
|
+
const leftover = await boundedCall(coolify.findApplicationByName(input.jobName, { environmentIds: [environment.id] }), 'Coolify findApplicationByName');
|
|
101
|
+
if (leftover) {
|
|
102
|
+
await boundedCall(coolify.raw('DELETE', `/applications/${leftover.uuid}`), 'delete the job');
|
|
103
|
+
}
|
|
104
|
+
const server = await resolvePlatformServer(coolify);
|
|
105
|
+
const created = await boundedCall(coolify.createDockerfileApplication({
|
|
106
|
+
project_uuid: projectUuid,
|
|
107
|
+
server_uuid: server.uuid,
|
|
108
|
+
environment_name: environment.name,
|
|
109
|
+
environment_uuid: environment.uuid,
|
|
110
|
+
dockerfile: brokerDbJobDockerfile(),
|
|
111
|
+
name: input.jobName,
|
|
112
|
+
description: `makes the broker's MCP database (auth ADR 0002)`,
|
|
113
|
+
instant_deploy: false,
|
|
114
|
+
autogenerate_domain: false,
|
|
115
|
+
}), 'Coolify createDockerfileApplication');
|
|
116
|
+
jobAppId = created.uuid;
|
|
117
|
+
await coolify.updateApplication(jobAppId, { health_check_enabled: false });
|
|
118
|
+
await coolify.bulkUpsertEnvs(jobAppId, [
|
|
119
|
+
{ key: JOB_ADMIN_ENV, value: input.adminUrl },
|
|
120
|
+
{ key: JOB_PASSWORD_ENV, value: password },
|
|
121
|
+
...(input.caPem
|
|
122
|
+
? [{ key: JOB_CA_ENV, value: Buffer.from(input.caPem, 'utf8').toString('base64') }]
|
|
123
|
+
: []),
|
|
124
|
+
]);
|
|
125
|
+
await coolify.deploy(jobAppId, true);
|
|
126
|
+
await waitForJob(coolify, jobAppId, runtime, {
|
|
127
|
+
job: "the broker's database job",
|
|
128
|
+
stopped: "the broker's database job stopped before it finished",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const why = error instanceof JobFailure ? error.message : 'a Coolify call failed or timed out';
|
|
133
|
+
let cleaned = '';
|
|
134
|
+
if (jobAppId) {
|
|
135
|
+
const id = jobAppId;
|
|
136
|
+
try {
|
|
137
|
+
await boundedCall(coolify.raw('POST', `/applications/${id}/stop`), 'stop the job');
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
cleaned += '; the job app could not be stopped, stop it in Coolify';
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await boundedCall(coolify.bulkUpsertEnvs(id, [JOB_ADMIN_ENV, JOB_PASSWORD_ENV, JOB_CA_ENV].map((key) => ({ key, value: '' }))), 'clear the job env');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
cleaned +=
|
|
147
|
+
'; its env could not be cleared, delete the app in Coolify (it holds the admin URL)';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`${label}: ${why}${jobAppId ? `; the job app ${jobAppId} is left stopped without secrets and the next apply replaces it${cleaned}` : ''}; apply again`);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
await boundedCall(coolify.raw('DELETE', `/applications/${jobAppId}`), 'delete the job');
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw new Error(`${label}: the broker's database job ran, but its app ${jobAppId} could not be deleted; delete it in Coolify (it holds the admin URL), then apply again`);
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -270,3 +270,15 @@ export declare const JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY = "jobs_registratio
|
|
|
270
270
|
* {@link effectiveAiLimitMicros} is the one place that substitutes it.
|
|
271
271
|
*/
|
|
272
272
|
export declare const DEFAULT_AI_LIMIT_MICROS = "112250000";
|
|
273
|
+
/**
|
|
274
|
+
* F5: one broker `AUTH_APPS` entry per entry with `uses.auth`. Idempotent on
|
|
275
|
+
* the recorded marker of what the last write said (ids, names, origins; never
|
|
276
|
+
* the key), so a rerun plans nothing. The values it needs come from steps
|
|
277
|
+
* that run earlier; one that nothing records and no step in this plan will
|
|
278
|
+
* record blocks it (rule 2). A foreign broker entry the last write found
|
|
279
|
+
* invalid is listed as a warning: the broker drops it, foundry keeps it.
|
|
280
|
+
*/
|
|
281
|
+
export declare function planBrokerEntries(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], brokerWired: boolean): {
|
|
282
|
+
steps: DraftStep[];
|
|
283
|
+
blocked: UnmanagedItem[];
|
|
284
|
+
};
|
package/dist/plan/diff.js
CHANGED
|
@@ -3,7 +3,7 @@ import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, held
|
|
|
3
3
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
4
4
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, chatCallsApplied, chatHasUpstreams, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
5
5
|
import { planBlueGreen } from './blue-green.js';
|
|
6
|
-
import { BOULE_ARCHON_CREDENTIAL_ENV, BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_CUSTOMERS_CREDENTIAL_ENV, BOULE_MAIL_TOKEN_ENV, BOULE_OWNER_SEED_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, CUSTOMERS_SIGN_IN_SCOPE_KEY, CUSTOMERS_SIGN_UP_SCOPE_KEY, archonCredentialRenewDue, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, customersOrgIdOf, desiredBouleEnv, planBouleOwners, staleCustomersEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
6
|
+
import { BOULE_ARCHON_CREDENTIAL_ENV, BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_CUSTOMERS_CREDENTIAL_ENV, BOULE_MAIL_TOKEN_ENV, BOULE_OWNER_SEED_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, CUSTOMERS_ROOM_SCOPE_KEY, CUSTOMERS_SIGN_IN_SCOPE_KEY, CUSTOMERS_SIGN_UP_SCOPE_KEY, archonCredentialRenewDue, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, customersOrgIdOf, customersRoomValue, desiredBouleEnv, planBouleOwners, staleCustomersEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
7
7
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
8
8
|
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
9
9
|
import { planCompanyBackups } from './company-backups.js';
|
|
@@ -18,7 +18,7 @@ import { hasWildcardGate, planGates } from './gates.js';
|
|
|
18
18
|
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
19
19
|
import { APP_GONE_KIND } from './live.js';
|
|
20
20
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
21
|
-
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, agoraIssuerHostFor, clientOrganizationsState, companyLoginTunnelHost, customersClientOrganizations, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
21
|
+
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, agoraIssuerHostFor, clientOrganizationsState, companyLoginTunnelHost, customersClientOrganizations, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, ownCustomersAppState, pendingRemovalHosts, } from './login-host.js';
|
|
22
22
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
23
23
|
import { mailStoreSwitchChange } from './mail-store.js';
|
|
24
24
|
import { planMcp } from './mcp.js';
|
|
@@ -28,7 +28,7 @@ import { legacyUnmigrated, orgPostgresUndeliveredScopeKey, planOrgPostgresSteps,
|
|
|
28
28
|
import { OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, OWN_SIGNIN_DB_MOVE_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, planOwnSfu, planSigninDbMove, } from './own-diff.js';
|
|
29
29
|
import { ownDnsHostnames } from './own-dns.js';
|
|
30
30
|
import { ownHostnames } from './own-hostnames.js';
|
|
31
|
-
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_FILES_INTERNAL_URL_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSecretAvailability, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
31
|
+
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, appSecretAvailability, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
32
32
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
33
33
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
34
34
|
import { companyAppPostLogoutUris, postLogoutUpdateOwed, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
@@ -1394,8 +1394,13 @@ function planOwnLoginHost(org, recordedIds, gates) {
|
|
|
1394
1394
|
const none = { step: null, blocked: null };
|
|
1395
1395
|
const mapOwed = clientOrganizationsOwed(org, recordedIds, true);
|
|
1396
1396
|
// Nothing wanted and nothing owed.
|
|
1397
|
-
|
|
1397
|
+
// A customers entry's app owes its base URI (the company's login host) until the marker names it.
|
|
1398
|
+
const customersOwed = Boolean(recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY]) &&
|
|
1399
|
+
ownCustomersAppState(org, recordedIds) !==
|
|
1400
|
+
(recordedIds[OWN_CUSTOMERS_BASE_URI_SCOPE_KEY] ?? '');
|
|
1401
|
+
if (!inAppSignIn(org) && !recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] && !mapOwed && !customersOwed) {
|
|
1398
1402
|
return none;
|
|
1403
|
+
}
|
|
1399
1404
|
// The company's own sign-in is reported on its own line (`coolify:own-auth`).
|
|
1400
1405
|
if (!ownAuthApplied(org, recordedIds))
|
|
1401
1406
|
return none;
|
|
@@ -1411,6 +1416,7 @@ function planOwnLoginHost(org, recordedIds, gates) {
|
|
|
1411
1416
|
if ((recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1412
1417
|
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
|
|
1413
1418
|
!mapOwed &&
|
|
1419
|
+
!customersOwed &&
|
|
1414
1420
|
pendingRemovalHosts(recordedIds).length === 0) {
|
|
1415
1421
|
return { step: null, blocked: waiting };
|
|
1416
1422
|
}
|
|
@@ -3955,7 +3961,7 @@ function planEntryClients(org, byKey, claimed) {
|
|
|
3955
3961
|
* record blocks it (rule 2). A foreign broker entry the last write found
|
|
3956
3962
|
* invalid is listed as a warning: the broker drops it, foundry keeps it.
|
|
3957
3963
|
*/
|
|
3958
|
-
function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired) {
|
|
3964
|
+
export function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired) {
|
|
3959
3965
|
const steps = [];
|
|
3960
3966
|
const blocked = [];
|
|
3961
3967
|
for (const entry of org.hosting.entries) {
|
|
@@ -3978,6 +3984,16 @@ function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired) {
|
|
|
3978
3984
|
});
|
|
3979
3985
|
continue;
|
|
3980
3986
|
}
|
|
3987
|
+
// Auth ADR 0002: a broker given an `mcp` entry before it holds its database and secret exits at boot.
|
|
3988
|
+
// A company's own sign-in step sets them and records it; a shared org's broker is the platform's, whose
|
|
3989
|
+
// marker this plan cannot see, so `writeBrokerEntry` refuses there until the platform step has run.
|
|
3990
|
+
if (auth.mcp && ownsServices(org) && recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] !== '1') {
|
|
3991
|
+
blocked.push({
|
|
3992
|
+
description: `${at} is blocked, not planned: the broker's MCP database and secret are not set yet (the sign-in step sets them)`,
|
|
3993
|
+
key,
|
|
3994
|
+
});
|
|
3995
|
+
continue;
|
|
3996
|
+
}
|
|
3981
3997
|
// A Worker app has no Coolify app to hold the broker key: its generated AUTH_APP_KEY
|
|
3982
3998
|
// is delivered to the Worker first and read back from the held-key store.
|
|
3983
3999
|
if (entry.workerApp && !brokerKeyDelivered(recordedIds, entry.key)) {
|
|
@@ -4102,13 +4118,16 @@ function planCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
|
4102
4118
|
const wantedSignIn = wanted && org.hosting.entries.some((entry) => entry.uses?.auth?.signIn === 'email-code')
|
|
4103
4119
|
? 'email-code'
|
|
4104
4120
|
: '';
|
|
4121
|
+
const room = org.hosting.entries.find((entry) => entry.uses?.auth?.room)?.uses?.auth?.room;
|
|
4122
|
+
const wantedRoom = room ? customersRoomValue(room) : '';
|
|
4105
4123
|
const blocked = [];
|
|
4106
4124
|
const customersOrgId = recordedIds[resourceKey('zitadel', 'customers', org.id)];
|
|
4107
4125
|
if (!customersOrgId) {
|
|
4108
4126
|
if (!wanted)
|
|
4109
4127
|
return { step: null, blocked };
|
|
4110
4128
|
const signIn = wantedSignIn === 'email-code' && metadataWired ? { signIn: wantedSignIn } : {};
|
|
4111
|
-
|
|
4129
|
+
const roomDesired = wantedRoom && metadataWired ? { room: wantedRoom } : {};
|
|
4130
|
+
if ((wantedSignIn === 'email-code' || wantedRoom) && !metadataWired) {
|
|
4112
4131
|
blocked.push(signInMetadataBlocked(org));
|
|
4113
4132
|
}
|
|
4114
4133
|
return {
|
|
@@ -4117,7 +4136,7 @@ function planCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
|
4117
4136
|
system: 'zitadel',
|
|
4118
4137
|
kind: 'customers',
|
|
4119
4138
|
key: org.id,
|
|
4120
|
-
desired: { signUp, ...signIn },
|
|
4139
|
+
desired: { signUp, ...signIn, ...roomDesired },
|
|
4121
4140
|
},
|
|
4122
4141
|
step: {
|
|
4123
4142
|
description: `provision a ZITADEL org for ${org.slug}'s customers, with self-registration ${signUp ? 'on' : 'off'}${signIn.signIn ? ' and sign-in by emailed code' : ''}`,
|
|
@@ -4141,10 +4160,12 @@ function planCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
|
4141
4160
|
const wantedSignUp = wanted && signUp ? '1' : '0';
|
|
4142
4161
|
const appliedSignIn = recordedIds[CUSTOMERS_SIGN_IN_SCOPE_KEY] === 'email-code' ? 'email-code' : '';
|
|
4143
4162
|
const signInDiffers = appliedSignIn !== wantedSignIn;
|
|
4144
|
-
|
|
4163
|
+
const roomDiffers = (recordedIds[CUSTOMERS_ROOM_SCOPE_KEY] ?? '') !== wantedRoom;
|
|
4164
|
+
if ((signInDiffers || roomDiffers) && !metadataWired)
|
|
4145
4165
|
blocked.push(signInMetadataBlocked(org));
|
|
4146
4166
|
const writeSignIn = signInDiffers && metadataWired;
|
|
4147
|
-
|
|
4167
|
+
const writeRoom = roomDiffers && metadataWired;
|
|
4168
|
+
if (applied === wantedSignUp && !writeSignIn && !writeRoom)
|
|
4148
4169
|
return { step: null, blocked };
|
|
4149
4170
|
return {
|
|
4150
4171
|
step: {
|
|
@@ -4155,11 +4176,14 @@ function planCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
|
4155
4176
|
desired: {
|
|
4156
4177
|
signUp: wantedSignUp === '1',
|
|
4157
4178
|
...(writeSignIn ? { signIn: wantedSignIn } : {}),
|
|
4179
|
+
...(writeRoom ? { room: wantedRoom } : {}),
|
|
4158
4180
|
},
|
|
4159
4181
|
},
|
|
4160
4182
|
step: {
|
|
4161
4183
|
description: applied === wantedSignUp
|
|
4162
|
-
?
|
|
4184
|
+
? writeSignIn
|
|
4185
|
+
? `${wantedSignIn ? 'set' : 'remove'} the emailed-code sign-in metadata of ${org.slug}'s customers`
|
|
4186
|
+
: `${wantedRoom ? 'set' : 'remove'} the sign-in window text of ${org.slug}'s customers`
|
|
4163
4187
|
: `turn self-registration ${wantedSignUp === '1' ? 'on' : 'off'} for ${org.slug}'s customers`,
|
|
4164
4188
|
destroys: false,
|
|
4165
4189
|
migrates: false,
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -2313,6 +2313,13 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2313
2313
|
* a re-run after a crash finishes the job.
|
|
2314
2314
|
*/
|
|
2315
2315
|
private zitadelCustomers;
|
|
2316
|
+
/**
|
|
2317
|
+
* `wtfalch.room` on the customers organisation: the planner's value (JSON)
|
|
2318
|
+
* while an entry declares a room, removed when none does. The planner puts
|
|
2319
|
+
* `room` in the step only when the recorded value differs. Returns the value
|
|
2320
|
+
* to record (empty after a removal), or null for no change.
|
|
2321
|
+
*/
|
|
2322
|
+
private writeCustomersRoomMetadata;
|
|
2316
2323
|
/**
|
|
2317
2324
|
* `wtfalch.signIn = email-code` on the customers organisation while an entry
|
|
2318
2325
|
* declares it, removed when none does. The planner puts `signIn` in the step
|
package/dist/plan/executor.js
CHANGED
|
@@ -10,7 +10,7 @@ import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID
|
|
|
10
10
|
import { applyBlueGreenConnectors, applyBlueGreenServer, applyBlueGreenWeights, } from './blue-green-apply.js';
|
|
11
11
|
import { actionsKeyName, bgKey, blueGreenSubjects, colorAppName, healthTokenName, liveAppNameFor, mintActionsKey, splitServers, } from './blue-green.js';
|
|
12
12
|
import { BouleOwnersError, httpBouleOwners } from './boule-owners-http.js';
|
|
13
|
-
import { BOULE_ARCHON_CREDENTIAL_ENV, BOULE_ARCHON_PUSHED_AT_SCOPE_KEY, BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_CUSTOMERS_CREDENTIAL_ENV, BOULE_CUSTOMERS_ORG_ENV, BOULE_DATABASE_SCOPE_KEY, BOULE_DEPLOYED_SCOPE_KEY, BOULE_OWNER_SEED_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, CUSTOMERS_DIRECTORY_USER_SCOPE_KEY, CUSTOMERS_SIGN_IN_METADATA_KEY, CUSTOMERS_SIGN_IN_SCOPE_KEY, CUSTOMERS_SIGN_UP_SCOPE_KEY, archonCredentialRenewDue, bouleArchonCredentialRef, bouleEnvDrift, bouleHostnameFor, bouleMailWanted, bouleOwnerRefusedScopeKey, bouleOwnerSeededScopeKey, bouleOwnerSeeds, customersDirectoryRef, customersOrgIdOf, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
13
|
+
import { BOULE_ARCHON_CREDENTIAL_ENV, BOULE_ARCHON_PUSHED_AT_SCOPE_KEY, BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_CUSTOMERS_CREDENTIAL_ENV, BOULE_CUSTOMERS_ORG_ENV, BOULE_DATABASE_SCOPE_KEY, BOULE_DEPLOYED_SCOPE_KEY, BOULE_OWNER_SEED_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, CUSTOMERS_DIRECTORY_USER_SCOPE_KEY, CUSTOMERS_ROOM_METADATA_KEY, CUSTOMERS_ROOM_SCOPE_KEY, CUSTOMERS_SIGN_IN_METADATA_KEY, CUSTOMERS_SIGN_IN_SCOPE_KEY, CUSTOMERS_SIGN_UP_SCOPE_KEY, archonCredentialRenewDue, bouleArchonCredentialRef, bouleEnvDrift, bouleHostnameFor, bouleMailWanted, bouleOwnerRefusedScopeKey, bouleOwnerSeededScopeKey, bouleOwnerSeeds, customersDirectoryRef, customersOrgIdOf, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
14
14
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, mcpCallbackUri, wantedBrokerEntry, writeBrokerEntry, } from './broker-entries.js';
|
|
15
15
|
import { recordBuiltCommit } from './build-record.js';
|
|
16
16
|
import { MAS_TAG, SYNAPSE_IMAGE, SYNAPSE_TAG, callsMasConfigYaml, callsServerName, callsSynapseHomeserverYaml, masDockerfile, mintCallsMatrixSecrets, parseCallsDbUrl, } from './calls-matrix.js';
|
|
@@ -37,7 +37,7 @@ import { GATE_CLAIM_NAME, gateHostnamesScopeKey, gateIdRecordKey, isBehindGate }
|
|
|
37
37
|
import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, wgetProbe, } from './health-check-settings.js';
|
|
38
38
|
import { PROXY_LABEL_TOKEN_REF } from './held-key-refs.js';
|
|
39
39
|
import { appHostname, cmsStudioHostnameFor, coolifyTunnelDomains, entryHostnames, siteHostnameFor, tunnelDnsHostnames, tunnelIngressHostnames, } from './hostnames.js';
|
|
40
|
-
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
40
|
+
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
41
41
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
42
42
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, normalizeMailTarget, } from './mail-relay.js';
|
|
43
43
|
import { applyAccessTokenLifetime, applyMcpAccess, applyMcpProject } from './mcp.js';
|
|
@@ -50,7 +50,7 @@ import { LIMITS, limited } from './own-limits.js';
|
|
|
50
50
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgNetwork, orgTarget, scopeFromOrg, } from './own-services.js';
|
|
51
51
|
import { ownSfuUrls, provisionSfuApp } from './own-sfu.js';
|
|
52
52
|
import { PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, } from './platform-diff.js';
|
|
53
|
-
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, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appFolderDiffers, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
53
|
+
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, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appFolderDiffers, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, orgDeclaresMcp, resolveAppEnv, serviceDbEnvAppliedScopeKey, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
54
54
|
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_MAIL_SERVER_SCOPE_KEY, defaultStalwartForInstance, filesDepsInvalid, filesShareDomains, provisionAuthStack, provisionDatabaseBackup, provisionFilesHost, provisionJobsHost, provisionMailServer, resolveOwnFilesKeys, } from './platform-providers.js';
|
|
55
55
|
import { companyAppPostLogoutUris, postLogoutAppliedValue, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
56
56
|
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, ENTRY_CONTAINER_LABELS, appLabelsAppliedScopeKey, builtinRoutes, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryProxyLabels, entryRoutes, entryRoutesMarker, ownsOikosName, routesWithoutToken, sameLabelSets, } from './proxy-labels.js';
|
|
@@ -2268,6 +2268,17 @@ export class FoundryStepExecutor {
|
|
|
2268
2268
|
customersAppIds.add(appId);
|
|
2269
2269
|
}
|
|
2270
2270
|
}
|
|
2271
|
+
// A company's own instance: a customers entry's app signs in on the company's login host. Without a base
|
|
2272
|
+
// URI of its own it falls back to the v1 login once in-app sign-in turns `loginV2.required` off.
|
|
2273
|
+
const ownCustomersAppIds = new Set();
|
|
2274
|
+
if (own) {
|
|
2275
|
+
for (const key of customersKeys) {
|
|
2276
|
+
const appId = recorded[`zitadel:app:${key}`];
|
|
2277
|
+
if (appId)
|
|
2278
|
+
ownCustomersAppIds.add(appId);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
const ownCustomersSet = [];
|
|
2271
2282
|
if (projectId) {
|
|
2272
2283
|
const listed = await this.ports.zitadel.listApps(zitadelOrgId, projectId);
|
|
2273
2284
|
listingComplete = listed.complete;
|
|
@@ -2277,6 +2288,11 @@ export class FoundryStepExecutor {
|
|
|
2277
2288
|
await setOidcAppLoginBaseUri(this.ports.zitadel, zitadelOrgId, projectId, app.id, sharedBaseUri);
|
|
2278
2289
|
continue;
|
|
2279
2290
|
}
|
|
2291
|
+
if (ownCustomersAppIds.has(app.id)) {
|
|
2292
|
+
await setOidcAppLoginBaseUri(this.ports.zitadel, zitadelOrgId, projectId, app.id, `https://${host}/`);
|
|
2293
|
+
ownCustomersSet.push(app.id);
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2280
2296
|
if (!boundAppIds.has(app.id))
|
|
2281
2297
|
continue;
|
|
2282
2298
|
// A company's own instance has the login host as its default; only the in-app apps move.
|
|
@@ -2317,9 +2333,11 @@ export class FoundryStepExecutor {
|
|
|
2317
2333
|
for (const app of liveApps) {
|
|
2318
2334
|
const uri = customersAppIds.has(app.id) && sharedBaseUri
|
|
2319
2335
|
? sharedBaseUri
|
|
2320
|
-
:
|
|
2321
|
-
?
|
|
2322
|
-
: app.
|
|
2336
|
+
: ownCustomersAppIds.has(app.id)
|
|
2337
|
+
? `https://${host}/`
|
|
2338
|
+
: boundAppIds.has(app.id)
|
|
2339
|
+
? baseUriOf(app.id)
|
|
2340
|
+
: app.loginBaseUri;
|
|
2323
2341
|
if (typeof uri === 'string')
|
|
2324
2342
|
currentLoginBaseUris.add(normalizedLoginBaseUri(uri));
|
|
2325
2343
|
}
|
|
@@ -2413,6 +2431,10 @@ export class FoundryStepExecutor {
|
|
|
2413
2431
|
if (orgsWritable) {
|
|
2414
2432
|
context.recordScopeId(LOGIN_CLIENT_ORGS_SCOPE_KEY, clientOrganizationsState(wantedOrgs));
|
|
2415
2433
|
}
|
|
2434
|
+
if (own) {
|
|
2435
|
+
// Written last: a failed base URI write leaves it behind and the step is planned again.
|
|
2436
|
+
context.recordScopeId(OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, ownCustomersSet.sort().join(','));
|
|
2437
|
+
}
|
|
2416
2438
|
context.recordScopeId(LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, pendingRemovalState(stillPending));
|
|
2417
2439
|
return { resourceId: host };
|
|
2418
2440
|
}
|
|
@@ -2487,13 +2509,47 @@ export class FoundryStepExecutor {
|
|
|
2487
2509
|
else
|
|
2488
2510
|
await this.ports.zitadel.closeSelfRegistration(customers.id);
|
|
2489
2511
|
await this.ports.zitadel.findOrCreateProjectGrant(zitadelOrgId, projectId, customers.id);
|
|
2490
|
-
// The sign-in metadata
|
|
2512
|
+
// The sign-in and room metadata come before every marker: a failed write leaves both markers behind, so the next plan repeats the step.
|
|
2491
2513
|
const signIn = await this.writeCustomersSignInMetadata(org, customers.id, action, context);
|
|
2514
|
+
const room = await this.writeCustomersRoomMetadata(org, customers.id, action, context);
|
|
2492
2515
|
context.recordScopeId(CUSTOMERS_SIGN_UP_SCOPE_KEY, signUp ? '1' : '0');
|
|
2493
2516
|
if (signIn)
|
|
2494
2517
|
context.recordScopeId(CUSTOMERS_SIGN_IN_SCOPE_KEY, signIn);
|
|
2518
|
+
if (room !== null)
|
|
2519
|
+
context.recordScopeId(CUSTOMERS_ROOM_SCOPE_KEY, room);
|
|
2495
2520
|
return { resourceId: customers.id };
|
|
2496
2521
|
}
|
|
2522
|
+
/**
|
|
2523
|
+
* `wtfalch.room` on the customers organisation: the planner's value (JSON)
|
|
2524
|
+
* while an entry declares a room, removed when none does. The planner puts
|
|
2525
|
+
* `room` in the step only when the recorded value differs. Returns the value
|
|
2526
|
+
* to record (empty after a removal), or null for no change.
|
|
2527
|
+
*/
|
|
2528
|
+
async writeCustomersRoomMetadata(org, customersOrgId, action, context) {
|
|
2529
|
+
const want = action.desired.room;
|
|
2530
|
+
if (want === undefined)
|
|
2531
|
+
return null;
|
|
2532
|
+
if (typeof want !== 'string') {
|
|
2533
|
+
throw new Error(`zitadel:customers for ${org.slug}: the planned sign-in window text is invalid`);
|
|
2534
|
+
}
|
|
2535
|
+
const { setOrgMetadata, removeOrgMetadata } = this.ports.zitadel;
|
|
2536
|
+
if (!setOrgMetadata || !removeOrgMetadata) {
|
|
2537
|
+
throw new Error(`zitadel:customers for ${org.slug}: the ZITADEL client cannot set organisation metadata`);
|
|
2538
|
+
}
|
|
2539
|
+
try {
|
|
2540
|
+
if (want !== '') {
|
|
2541
|
+
await setOrgMetadata.call(this.ports.zitadel, customersOrgId, CUSTOMERS_ROOM_METADATA_KEY, want);
|
|
2542
|
+
return want;
|
|
2543
|
+
}
|
|
2544
|
+
if (!context.recordedIds()[CUSTOMERS_ROOM_SCOPE_KEY])
|
|
2545
|
+
return null;
|
|
2546
|
+
await removeOrgMetadata.call(this.ports.zitadel, customersOrgId, CUSTOMERS_ROOM_METADATA_KEY);
|
|
2547
|
+
return '';
|
|
2548
|
+
}
|
|
2549
|
+
catch {
|
|
2550
|
+
throw new Error(`zitadel:customers for ${org.slug}: writing the sign-in window text of organisation ${customersOrgId} failed`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2497
2553
|
/**
|
|
2498
2554
|
* `wtfalch.signIn = email-code` on the customers organisation while an entry
|
|
2499
2555
|
* declares it, removed when none does. The planner puts `signIn` in the step
|
|
@@ -7285,6 +7341,7 @@ export class FoundryStepExecutor {
|
|
|
7285
7341
|
// In-app sign-in: the company's own stack gets the second login app. Its host table starts
|
|
7286
7342
|
// empty; the login-host step writes it once the apps' clients exist.
|
|
7287
7343
|
...(inAppSignIn(org) ? { signInLogin: { bindings: null } } : {}),
|
|
7344
|
+
mcp: orgDeclaresMcp(org),
|
|
7288
7345
|
});
|
|
7289
7346
|
if (refChanged) {
|
|
7290
7347
|
const now = scope.recordedIds();
|
|
@@ -184,6 +184,14 @@ export declare const LOGIN_CLIENT_ORGS_SCOPE_KEY = "login_client_orgs";
|
|
|
184
184
|
* recorded. Staff clients are not here.
|
|
185
185
|
*/
|
|
186
186
|
export declare function customersClientOrganizations(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): Record<string, string>;
|
|
187
|
+
/**
|
|
188
|
+
* Scope id holding the ZITADEL app ids of an own org's customers entries whose Login V2 base URI was set to the
|
|
189
|
+
* company's login host (sorted, joined by `,`). On the company's own instance an app with no base URI of its own
|
|
190
|
+
* falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
|
|
191
|
+
*/
|
|
192
|
+
export declare const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = "own_customers_login_base_uri";
|
|
193
|
+
/** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
|
|
194
|
+
export declare function ownCustomersAppState(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): string;
|
|
187
195
|
/** {@link LOGIN_CLIENT_ORGS_SCOPE_KEY}'s value for a map. */
|
|
188
196
|
export declare function clientOrganizationsState(map: Readonly<Record<string, string>>): string;
|
|
189
197
|
/** The client ids a recorded {@link LOGIN_CLIENT_ORGS_SCOPE_KEY} value holds. */
|
package/dist/plan/login-host.js
CHANGED
|
@@ -211,6 +211,20 @@ export function customersClientOrganizations(org, recorded) {
|
|
|
211
211
|
}
|
|
212
212
|
return map;
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Scope id holding the ZITADEL app ids of an own org's customers entries whose Login V2 base URI was set to the
|
|
216
|
+
* company's login host (sorted, joined by `,`). On the company's own instance an app with no base URI of its own
|
|
217
|
+
* falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
|
|
218
|
+
*/
|
|
219
|
+
export const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = 'own_customers_login_base_uri';
|
|
220
|
+
/** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
|
|
221
|
+
export function ownCustomersAppState(org, recorded) {
|
|
222
|
+
return [...customersEntryKeys(org)]
|
|
223
|
+
.map((key) => recorded[`zitadel:app:${key}`])
|
|
224
|
+
.filter((id) => Boolean(id))
|
|
225
|
+
.sort()
|
|
226
|
+
.join(',');
|
|
227
|
+
}
|
|
214
228
|
/** {@link LOGIN_CLIENT_ORGS_SCOPE_KEY}'s value for a map. */
|
|
215
229
|
export function clientOrganizationsState(map) {
|
|
216
230
|
return Object.entries(map)
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -10,7 +10,7 @@ import { mailStoreIsLegacy, mailStoreSwitchDescription } from './mail-store.js';
|
|
|
10
10
|
import { ownHostnames } from './own-hostnames.js';
|
|
11
11
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
|
|
12
12
|
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';
|
|
13
|
-
import { 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_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_SMTP_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, platformMailMxHost, } from './platform-diff.js';
|
|
13
|
+
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_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_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, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
14
14
|
import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
|
|
15
15
|
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
16
16
|
import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn } from './sign-in.js';
|
|
@@ -162,6 +162,9 @@ export function planOwnAuth(options) {
|
|
|
162
162
|
recordedIds[PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY] &&
|
|
163
163
|
recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY] &&
|
|
164
164
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
165
|
+
recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
|
|
166
|
+
// An entry with `mcp` needs the broker's database and secret (auth ADR 0002).
|
|
167
|
+
(!orgDeclaresMcp(org) || recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] === '1') &&
|
|
165
168
|
(Boolean(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]) || !brokerHoldsAnEntry) &&
|
|
166
169
|
// In-app sign-in: the second login app is recorded once it is deployed and holds the token, which
|
|
167
170
|
// needs the login client; until the broker holds an entry that cannot be, so the step waits.
|
|
@@ -60,6 +60,22 @@ export declare const PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY = "auth_z
|
|
|
60
60
|
export declare const AUTH_ZITADEL_RUN_OPTIONS = "--entrypoint \"/app/zitadel start-from-init --masterkeyFromEnv --tlsMode external\"";
|
|
61
61
|
/** The run options last applied to the ZITADEL app (value: the options string), so a changed value forces one redeploy. */
|
|
62
62
|
export declare const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = "zitadel_platform_auth_run_options_applied";
|
|
63
|
+
/**
|
|
64
|
+
* The version of the sign-in tunnel's path rules (`provisionAuthStack`). Raise it when a rule
|
|
65
|
+
* changes, so an applied sign-in plans its step once more and the tunnel is written again.
|
|
66
|
+
* 2: the broker's MCP authorization paths (auth ADR 0002).
|
|
67
|
+
*/
|
|
68
|
+
export declare const AUTH_TUNNEL_ROUTES_VERSION = "2";
|
|
69
|
+
/** The {@link AUTH_TUNNEL_ROUTES_VERSION} the sign-in tunnel was last written with. */
|
|
70
|
+
export declare const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = "auth_tunnel_routes_applied";
|
|
71
|
+
/**
|
|
72
|
+
* `'1'` once the broker holds `MCP_DATABASE_URL` and `MCP_OAUTH_SECRET` (auth ADR 0002), recorded by the
|
|
73
|
+
* sign-in step when an entry of that broker declares `uses.auth.mcp`. A broker entry with `mcp` is not
|
|
74
|
+
* written before it, and an applied sign-in that wants it and lacks it plans its step again.
|
|
75
|
+
*/
|
|
76
|
+
export declare const PLATFORM_AUTH_MCP_SCOPE_KEY = "auth_mcp_env_applied";
|
|
77
|
+
/** True when a hosting entry of the org declares `uses.auth.mcp`: its broker needs the MCP database and secret. */
|
|
78
|
+
export declare function orgDeclaresMcp(org: Pick<DeclaredOrg, 'hosting'>): boolean;
|
|
63
79
|
/**
|
|
64
80
|
* The `gitRef` the platform's login, in-app login and broker apps were last built from (value: the ref). A
|
|
65
81
|
* different declared ref plans the auth step again: it patches `git_branch` on those three apps and
|
|
@@ -65,6 +65,24 @@ export const PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY = 'auth_zitadel_a
|
|
|
65
65
|
export const AUTH_ZITADEL_RUN_OPTIONS = '--entrypoint "/app/zitadel start-from-init --masterkeyFromEnv --tlsMode external"';
|
|
66
66
|
/** The run options last applied to the ZITADEL app (value: the options string), so a changed value forces one redeploy. */
|
|
67
67
|
export const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = 'zitadel_platform_auth_run_options_applied';
|
|
68
|
+
/**
|
|
69
|
+
* The version of the sign-in tunnel's path rules (`provisionAuthStack`). Raise it when a rule
|
|
70
|
+
* changes, so an applied sign-in plans its step once more and the tunnel is written again.
|
|
71
|
+
* 2: the broker's MCP authorization paths (auth ADR 0002).
|
|
72
|
+
*/
|
|
73
|
+
export const AUTH_TUNNEL_ROUTES_VERSION = '2';
|
|
74
|
+
/** The {@link AUTH_TUNNEL_ROUTES_VERSION} the sign-in tunnel was last written with. */
|
|
75
|
+
export const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = 'auth_tunnel_routes_applied';
|
|
76
|
+
/**
|
|
77
|
+
* `'1'` once the broker holds `MCP_DATABASE_URL` and `MCP_OAUTH_SECRET` (auth ADR 0002), recorded by the
|
|
78
|
+
* sign-in step when an entry of that broker declares `uses.auth.mcp`. A broker entry with `mcp` is not
|
|
79
|
+
* written before it, and an applied sign-in that wants it and lacks it plans its step again.
|
|
80
|
+
*/
|
|
81
|
+
export const PLATFORM_AUTH_MCP_SCOPE_KEY = 'auth_mcp_env_applied';
|
|
82
|
+
/** True when a hosting entry of the org declares `uses.auth.mcp`: its broker needs the MCP database and secret. */
|
|
83
|
+
export function orgDeclaresMcp(org) {
|
|
84
|
+
return org.hosting.enabled && org.hosting.entries.some((e) => Boolean(e.uses?.auth?.mcp));
|
|
85
|
+
}
|
|
68
86
|
/**
|
|
69
87
|
* The `gitRef` the platform's login, in-app login and broker apps were last built from (value: the ref). A
|
|
70
88
|
* different declared ref plans the auth step again: it patches `git_branch` on those three apps and
|
|
@@ -2127,6 +2145,7 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
2127
2145
|
// The ZITADEL run options reach the container only through the auth
|
|
2128
2146
|
// step's redeploy, so a platform recorded before them still plans it.
|
|
2129
2147
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
2148
|
+
recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
|
|
2130
2149
|
recordedIds[PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY] === platform.auth.gitRef) {
|
|
2131
2150
|
return { step: null, blocked: standingNote };
|
|
2132
2151
|
}
|
|
@@ -432,6 +432,11 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
432
432
|
* (or the runner was never applied).
|
|
433
433
|
*/
|
|
434
434
|
refreshFilesShare(_platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<Record<string, string> | null>;
|
|
435
|
+
/**
|
|
436
|
+
* True while a shared org declares `uses.auth.mcp`: its entry goes to the platform's broker, which then
|
|
437
|
+
* needs the MCP database and secret (auth ADR 0002). An own org's entry goes to its own broker.
|
|
438
|
+
*/
|
|
439
|
+
private mcpWanted;
|
|
435
440
|
/** True while a company signs in on its apps' own addresses through the platform's login. */
|
|
436
441
|
private signInLoginWanted;
|
|
437
442
|
/**
|
|
@@ -510,6 +515,11 @@ export declare const AUTH_LOGIN_PORT = "3000";
|
|
|
510
515
|
* everything else, including the root).
|
|
511
516
|
*/
|
|
512
517
|
export declare const AUTH_ZITADEL_PATH_PATTERN = "^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|ui/console|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)";
|
|
518
|
+
/**
|
|
519
|
+
* The broker's MCP authorization server (auth ADR 0002): its metadata and `/mcp-oauth/*`.
|
|
520
|
+
* Checked before {@link AUTH_ZITADEL_PATH_PATTERN}, which takes every other `/.well-known` path.
|
|
521
|
+
*/
|
|
522
|
+
export declare const AUTH_BROKER_MCP_PATH_PATTERN = "^/(\\.well-known/oauth-authorization-server|mcp-oauth/.+)$";
|
|
513
523
|
export declare const PLATFORM_AUTH_DB_SCOPE_KEY = "coolify_platform_auth_database";
|
|
514
524
|
export declare const PLATFORM_AUTH_ZITADEL_SCOPE_KEY = "coolify_platform_auth_zitadel_app";
|
|
515
525
|
export declare const PLATFORM_AUTH_LOGIN_SCOPE_KEY = "coolify_platform_auth_login_app";
|
|
@@ -549,6 +559,11 @@ export interface AuthStackInput {
|
|
|
549
559
|
signInLogin?: {
|
|
550
560
|
bindings: string | null;
|
|
551
561
|
};
|
|
562
|
+
/**
|
|
563
|
+
* An entry of this stack's broker declares `uses.auth.mcp` (auth ADR 0002): the broker needs a `broker`
|
|
564
|
+
* database on the sign-in Postgres and `MCP_OAUTH_SECRET` before that entry reaches it. Absent: neither.
|
|
565
|
+
*/
|
|
566
|
+
mcp?: boolean;
|
|
552
567
|
}
|
|
553
568
|
/**
|
|
554
569
|
* #168: makes `coolify:app` keyed {@link PLATFORM_AUTH_KEY} true -- a Coolify
|
|
@@ -1312,6 +1327,33 @@ export declare function envsDiffer(coolify: Pick<CoolifyPort, 'listEnvs'>, appId
|
|
|
1312
1327
|
key: string;
|
|
1313
1328
|
value: string;
|
|
1314
1329
|
}>): Promise<boolean>;
|
|
1330
|
+
/**
|
|
1331
|
+
* `null` once the app already has this key set -- generated only the first
|
|
1332
|
+
* time, never regenerated or overwritten, mirroring `executor.ts`'s
|
|
1333
|
+
* `ensureAuthCookieSecret` exactly. #211 round 4: `''`, `null` and
|
|
1334
|
+
* `undefined` all count as not set, the same "missing" a live Coolify
|
|
1335
|
+
* response can carry despite its declared `string` type
|
|
1336
|
+
* (`CoolifyPort.listEnvs`'s own doc comment) -- an entry present with one of
|
|
1337
|
+
* those values used to read as "already has this key set" and return `null`,
|
|
1338
|
+
* so a caller reading the value back afterwards (`provisionPlatformArchon`'s
|
|
1339
|
+
* own `AUTH_APP_KEY`) threw "lost its ... between two reads" on every single
|
|
1340
|
+
* apply instead of regenerating it.
|
|
1341
|
+
*
|
|
1342
|
+
* #211 round 5: `minLength`, when given, also counts a *present, non-empty*
|
|
1343
|
+
* value shorter than it as not set -- the broker's own `parseApps`
|
|
1344
|
+
* (`wtfalch/auth/broker/src/apps.ts`) refuses a `key` under 32 characters,
|
|
1345
|
+
* and this package minted a 32-character `AUTH_APP_KEY` from the start
|
|
1346
|
+
* (`provisionPlatformArchon`'s own comment on `randomBytes(32)`), so this
|
|
1347
|
+
* only ever changes behaviour for a key that reached the app some other way
|
|
1348
|
+
* -- a hand-set value, or one carried over from before that guarantee held.
|
|
1349
|
+
* Optional, and unused by every other call site (`ensureMasterkey`,
|
|
1350
|
+
* `ZITADEL_ADMIN_PASSWORD`, `AUTH_COOKIE_SECRET`, this file's recovery admin
|
|
1351
|
+
* password): none of those have a broker-enforced minimum, so widening this
|
|
1352
|
+
* check to them would reject a value that has always been fine.
|
|
1353
|
+
*/
|
|
1354
|
+
export declare function ensureGeneratedEnv(coolify: Pick<CoolifyPort, 'listEnvs'>, appId: string, key: string, generate: () => string, minLength?: number): Promise<string | null>;
|
|
1355
|
+
/** The live value of `key` on `appId`, or `undefined` when unset -- `ensureGeneratedEnv`'s read half, for a caller that needs the value it already found. */
|
|
1356
|
+
export declare function currentEnvValue(coolify: Pick<CoolifyPort, 'listEnvs'>, appId: string, key: string): Promise<string | undefined>;
|
|
1315
1357
|
/**
|
|
1316
1358
|
* #171: everything {@link provisionPlatformFiles} needs beyond
|
|
1317
1359
|
* {@link PlatformStepExecutionContext}, bundled for the same reason as
|
|
@@ -3,7 +3,7 @@ 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';
|
|
5
5
|
import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
6
|
-
import { ownerMailboxes, sharedSecretRef, } from '../org.js';
|
|
6
|
+
import { ownerMailboxes, ownsServices, sharedSecretRef, } from '../org.js';
|
|
7
7
|
import { applyArchonBlueGreen } from './archon-blue-green.js';
|
|
8
8
|
import { ARCHON_REPO, archonSubject, healthTokenName, tunnelOriginFor, } from './blue-green.js';
|
|
9
9
|
import { BOULE_BRIDGE_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_PEER_ROLE, bouleHostnameFor, } from './boule.js';
|
|
@@ -21,6 +21,7 @@ import { LIMITS, limited } from './own-limits.js';
|
|
|
21
21
|
import { postLogoutAppliedValue, postLogoutUrisFor } from './post-logout-uris.js';
|
|
22
22
|
import { createReleaseHost } from './release-host.js';
|
|
23
23
|
export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
24
|
+
import { BROKER_DB_NAME, BROKER_DB_USER, ensureBrokerMcpEnv } from './broker-mcp.js';
|
|
24
25
|
import { SIGN_IN_BIND_KEY_ENV, SIGN_IN_BIND_KEY_MIN_LENGTH, SIGN_IN_BIND_KEY_SCOPE_KEY, } from './cookie-domain.js';
|
|
25
26
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
26
27
|
import { MAIL_RELAY_BLOCKED, provisionMailRelay } from './mail-relay.js';
|
|
@@ -28,13 +29,13 @@ import { MAIL_APP_SCOPE_KEY, MAIL_DATABASE_NAME, MAIL_DATABASE_PASSWORD_ENV, MAI
|
|
|
28
29
|
import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
|
|
29
30
|
import { recordOwnDnsHostname } from './own-dns.js';
|
|
30
31
|
import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
|
|
31
|
-
import { 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_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_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_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_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_CHAT_ROOM_ID_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, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
32
|
+
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_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_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_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_CHAT_ROOM_ID_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, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
32
33
|
import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
|
|
33
34
|
import { LOGIN_HOST_BINDINGS_ENV } from './sign-in-bindings.js';
|
|
34
35
|
import { ensureSignInOrg, inviteOwnerWithLink, signInOrgDomain } from './sign-in-org.js';
|
|
35
36
|
import { SIGN_IN_BASE_PATH, SIGN_IN_BUILD_ARG_ENV, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_UPSTREAM_ENV, inAppOnSharedLogin, signInLoginNames, signInUpstream, } from './sign-in.js';
|
|
36
37
|
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY } from './signin-db-tls.js';
|
|
37
|
-
import { SIGNIN_DB_APP_SCOPE_KEY, SIGNIN_DB_TLS_SCOPE_KEY, dockerfileAppCreator, ensureSigninDbCaFile, provisionSigninDb, waitHealthy, withCallTimeout, } from './signin-db.js';
|
|
38
|
+
import { SIGNIN_DB_APP_SCOPE_KEY, SIGNIN_DB_TLS_SCOPE_KEY, dockerfileAppCreator, ensureSigninDbCaFile, provisionSigninDb, signinDbDsn, waitHealthy, withCallTimeout, } from './signin-db.js';
|
|
38
39
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
39
40
|
import { githubReleaseHookPlanInputs, provisionGithubReleaseHook, } from './github-release-hook.js';
|
|
40
41
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
@@ -723,7 +724,11 @@ export class FoundryPlatformStepExecutor {
|
|
|
723
724
|
}
|
|
724
725
|
if (platform.auth.enabled && this.auth) {
|
|
725
726
|
const loginClientDrifted = await authLoginClientDrifted(platform, this.auth, recordedIds);
|
|
726
|
-
|
|
727
|
+
// The broker lacks the MCP database and secret a shared org's entry needs (auth ADR 0002).
|
|
728
|
+
const mcpDrifted = recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] !== '1' && (await this.mcpWanted());
|
|
729
|
+
if (loginClientDrifted ||
|
|
730
|
+
mcpDrifted ||
|
|
731
|
+
(await this.signInLoginDrifted(this.auth, recordedIds))) {
|
|
727
732
|
result.auth = true;
|
|
728
733
|
}
|
|
729
734
|
if (await authAppsNotRunning(this.auth.coolify, recordedIds))
|
|
@@ -785,6 +790,13 @@ export class FoundryPlatformStepExecutor {
|
|
|
785
790
|
await coolify.deploy(runnerAppId);
|
|
786
791
|
return { [PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY]: normalHosts(wanted).join(',') };
|
|
787
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* True while a shared org declares `uses.auth.mcp`: its entry goes to the platform's broker, which then
|
|
795
|
+
* needs the MCP database and secret (auth ADR 0002). An own org's entry goes to its own broker.
|
|
796
|
+
*/
|
|
797
|
+
async mcpWanted() {
|
|
798
|
+
return (await this.declaredOrgStore.list()).some((org) => !ownsServices(org) && orgDeclaresMcp(org));
|
|
799
|
+
}
|
|
788
800
|
/** True while a company signs in on its apps' own addresses through the platform's login. */
|
|
789
801
|
async signInLoginWanted() {
|
|
790
802
|
return (await this.declaredOrgStore.list()).some(inAppOnSharedLogin);
|
|
@@ -826,7 +838,11 @@ export class FoundryPlatformStepExecutor {
|
|
|
826
838
|
throw new Error('platform:auth needs FoundryPlatformStepExecutor to be constructed with its auth dependencies (#168)');
|
|
827
839
|
}
|
|
828
840
|
const company = await this.declaredOrgStore.get(context.platform.ownerOrgId);
|
|
829
|
-
return provisionPlatformAuth(context, this.limitedOnPlatform(this.auth, LIMITS.platformAuth), {
|
|
841
|
+
return provisionPlatformAuth(context, this.limitedOnPlatform(this.auth, LIMITS.platformAuth), {
|
|
842
|
+
ownerCompany: company,
|
|
843
|
+
signInLogin: await this.signInLoginWanted(),
|
|
844
|
+
mcp: await this.mcpWanted(),
|
|
845
|
+
});
|
|
830
846
|
}
|
|
831
847
|
if (action.system === 'coolify' && action.kind === 'app' && action.key === PLATFORM_FILES_KEY) {
|
|
832
848
|
if (!this.files) {
|
|
@@ -1317,6 +1333,11 @@ export const AUTH_ZITADEL_PATH_PATTERN = '^/(oidc|oauth|saml|management|admin|sy
|
|
|
1317
1333
|
* than folded into it.
|
|
1318
1334
|
*/
|
|
1319
1335
|
const AUTH_BROKER_PATH_PATTERN = '^/api(/|$)';
|
|
1336
|
+
/**
|
|
1337
|
+
* The broker's MCP authorization server (auth ADR 0002): its metadata and `/mcp-oauth/*`.
|
|
1338
|
+
* Checked before {@link AUTH_ZITADEL_PATH_PATTERN}, which takes every other `/.well-known` path.
|
|
1339
|
+
*/
|
|
1340
|
+
export const AUTH_BROKER_MCP_PATH_PATTERN = '^/(\\.well-known/oauth-authorization-server|mcp-oauth/.+)$';
|
|
1320
1341
|
const PLATFORM_AUTH_PROJECT_SCOPE_KEY = 'coolify_project_platform_auth';
|
|
1321
1342
|
export const PLATFORM_AUTH_DB_SCOPE_KEY = 'coolify_platform_auth_database';
|
|
1322
1343
|
export const PLATFORM_AUTH_ZITADEL_SCOPE_KEY = 'coolify_platform_auth_zitadel_app';
|
|
@@ -1666,6 +1687,8 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1666
1687
|
recorded[PLATFORM_AUTH_ZITADEL_DEPLOYED_SCOPE_KEY]);
|
|
1667
1688
|
let databaseUrl;
|
|
1668
1689
|
let signinDbCa;
|
|
1690
|
+
/** The broker role's connection string on the sign-in Postgres, for a password (auth ADR 0002). */
|
|
1691
|
+
let brokerMcpUrl;
|
|
1669
1692
|
if (tlsDatabase) {
|
|
1670
1693
|
const provisioned = await provisionSigninDb({
|
|
1671
1694
|
coolify: deps.coolify,
|
|
@@ -1691,6 +1714,10 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1691
1714
|
});
|
|
1692
1715
|
databaseUrl = provisioned.dsn;
|
|
1693
1716
|
signinDbCa = provisioned.ca;
|
|
1717
|
+
brokerMcpUrl = (password) => signinDbDsn(target.internal.signinDb, password, {
|
|
1718
|
+
user: BROKER_DB_USER,
|
|
1719
|
+
database: BROKER_DB_NAME,
|
|
1720
|
+
});
|
|
1694
1721
|
await record(SIGNIN_DB_TLS_SCOPE_KEY, provisioned.appId);
|
|
1695
1722
|
}
|
|
1696
1723
|
else {
|
|
@@ -1717,6 +1744,14 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1717
1744
|
throw new Error(`coolify:app for ${subject} (auth): the database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
1718
1745
|
}
|
|
1719
1746
|
databaseUrl = db.internal_db_url;
|
|
1747
|
+
const legacyUrl = db.internal_db_url;
|
|
1748
|
+
brokerMcpUrl = (password) => {
|
|
1749
|
+
const url = new URL(legacyUrl);
|
|
1750
|
+
url.username = BROKER_DB_USER;
|
|
1751
|
+
url.password = password;
|
|
1752
|
+
url.pathname = `/${BROKER_DB_NAME}`;
|
|
1753
|
+
return url.toString();
|
|
1754
|
+
};
|
|
1720
1755
|
}
|
|
1721
1756
|
// ---- the ZITADEL app itself ---------------------------------------------
|
|
1722
1757
|
const zitadelApp = await findOrCreatePlatformApp(deps.coolify, target.names.authZitadel, environment, async () => {
|
|
@@ -1953,7 +1988,26 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1953
1988
|
// is, once held (#210): pushing it costs nothing even while the broker
|
|
1954
1989
|
// still cannot start on the other two, and the deploy decision below stays
|
|
1955
1990
|
// gated on `missingBrokerInputs` regardless.
|
|
1991
|
+
// Auth ADR 0002: a broker with an `mcp` entry exits without a `broker` database and `MCP_OAUTH_SECRET`, so
|
|
1992
|
+
// both are set here, before such an entry is written (`writeBrokerEntry` refuses until they are live).
|
|
1993
|
+
let mcpEnvs = [];
|
|
1994
|
+
if (input.mcp) {
|
|
1995
|
+
mcpEnvs = await ensureBrokerMcpEnv({
|
|
1996
|
+
coolify: deps.coolify,
|
|
1997
|
+
brokerAppId,
|
|
1998
|
+
jobName: `${target.names.authBroker}-mcp-db-job`,
|
|
1999
|
+
project: { uuid: projectUuid, environment },
|
|
2000
|
+
adminUrl: databaseUrl,
|
|
2001
|
+
caPem: signinDbCa,
|
|
2002
|
+
brokerUrl: brokerMcpUrl,
|
|
2003
|
+
runtime,
|
|
2004
|
+
label,
|
|
2005
|
+
});
|
|
2006
|
+
if (signinDbCa)
|
|
2007
|
+
await ensureSigninDbCaFile(deps.coolify, brokerAppId, signinDbCa, label);
|
|
2008
|
+
}
|
|
1956
2009
|
const brokerEnvs = [
|
|
2010
|
+
...mcpEnvs,
|
|
1957
2011
|
{
|
|
1958
2012
|
key: 'ZITADEL_API_URL',
|
|
1959
2013
|
value: `http://${target.internal.authZitadel}:${AUTH_ZITADEL_PORT}`,
|
|
@@ -2057,6 +2111,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2057
2111
|
await deps.coolify.deploy(brokerAppId);
|
|
2058
2112
|
await record(PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, brokerDeployRecordValue(brokerAppId, authAppsRaw));
|
|
2059
2113
|
}
|
|
2114
|
+
// The env is live on the broker (and deployed when it changed): the entry write may now carry `mcp`.
|
|
2115
|
+
if (input.mcp)
|
|
2116
|
+
await record(PLATFORM_AUTH_MCP_SCOPE_KEY, '1');
|
|
2060
2117
|
// ---- tunnel and DNS ------------------------------------------------------
|
|
2061
2118
|
const tunnels = await network.tunnels();
|
|
2062
2119
|
const existingTunnel = await tunnels.findTunnelByName(target.names.authTunnel);
|
|
@@ -2064,6 +2121,11 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2064
2121
|
await record(PLATFORM_AUTH_TUNNEL_SCOPE_KEY, tunnel.id);
|
|
2065
2122
|
await tunnels.putTunnelConfig(tunnel.id, {
|
|
2066
2123
|
ingress: [
|
|
2124
|
+
{
|
|
2125
|
+
hostname,
|
|
2126
|
+
path: AUTH_BROKER_MCP_PATH_PATTERN,
|
|
2127
|
+
service: `http://${target.internal.authBroker}:${AUTH_BROKER_PORT}`,
|
|
2128
|
+
},
|
|
2067
2129
|
{
|
|
2068
2130
|
hostname,
|
|
2069
2131
|
path: AUTH_ZITADEL_PATH_PATTERN,
|
|
@@ -2081,6 +2143,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2081
2143
|
{ service: 'http_status:404' },
|
|
2082
2144
|
],
|
|
2083
2145
|
});
|
|
2146
|
+
await record(PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, AUTH_TUNNEL_ROUTES_VERSION);
|
|
2084
2147
|
// ---- cloudflared connector for the tunnel above (#197) --------------------
|
|
2085
2148
|
// Without this, `hostname` resolves to a tunnel nothing is dialed into and
|
|
2086
2149
|
// answers with a Cloudflare error -- the known gap #197 closes for this
|
|
@@ -2551,7 +2614,7 @@ const platformAuthProject = {
|
|
|
2551
2614
|
};
|
|
2552
2615
|
/** The platform's sign-in: {@link provisionAuthStack} with {@link platformTarget} and the platform's own inputs. */
|
|
2553
2616
|
async function provisionPlatformAuth(context, deps, options = {}) {
|
|
2554
|
-
const { ownerCompany = null, signInLogin = false } = options;
|
|
2617
|
+
const { ownerCompany = null, signInLogin = false, mcp = false } = options;
|
|
2555
2618
|
const { platform } = context;
|
|
2556
2619
|
const scope = scopeFromPlatform(context);
|
|
2557
2620
|
return provisionAuthStack(scope, deps, platformTarget(), {
|
|
@@ -2565,6 +2628,7 @@ async function provisionPlatformAuth(context, deps, options = {}) {
|
|
|
2565
2628
|
ownerCompany,
|
|
2566
2629
|
// In-app sign-in: the second login app copies the login's host table (the host's port writes that).
|
|
2567
2630
|
...(signInLogin ? { signInLogin: { bindings: null } } : {}),
|
|
2631
|
+
mcp,
|
|
2568
2632
|
});
|
|
2569
2633
|
}
|
|
2570
2634
|
/**
|
|
@@ -7177,7 +7241,7 @@ export async function envsDiffer(coolify, appId, desired) {
|
|
|
7177
7241
|
* password): none of those have a broker-enforced minimum, so widening this
|
|
7178
7242
|
* check to them would reject a value that has always been fine.
|
|
7179
7243
|
*/
|
|
7180
|
-
async function ensureGeneratedEnv(coolify, appId, key, generate, minLength) {
|
|
7244
|
+
export async function ensureGeneratedEnv(coolify, appId, key, generate, minLength) {
|
|
7181
7245
|
const envs = await coolify.listEnvs(appId);
|
|
7182
7246
|
const entry = envs.find((e) => e.key === key && !e.is_preview);
|
|
7183
7247
|
const already = entry !== undefined &&
|
|
@@ -7189,7 +7253,7 @@ async function ensureGeneratedEnv(coolify, appId, key, generate, minLength) {
|
|
|
7189
7253
|
return generate();
|
|
7190
7254
|
}
|
|
7191
7255
|
/** The live value of `key` on `appId`, or `undefined` when unset -- `ensureGeneratedEnv`'s read half, for a caller that needs the value it already found. */
|
|
7192
|
-
async function currentEnvValue(coolify, appId, key) {
|
|
7256
|
+
export async function currentEnvValue(coolify, appId, key) {
|
|
7193
7257
|
const envs = await coolify.listEnvs(appId);
|
|
7194
7258
|
return envs.find((e) => e.key === key && !e.is_preview)?.value;
|
|
7195
7259
|
}
|