@wtfalch/foundry 0.71.0 → 0.72.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/modules/sign-in.js +81 -9
- package/dist/modules/types.d.ts +2 -0
- package/dist/org.d.ts +18 -1
- package/dist/org.js +40 -0
- package/dist/plan/boule.d.ts +15 -0
- package/dist/plan/boule.js +13 -0
- package/dist/plan/broker-registrar-token.d.ts +69 -0
- package/dist/plan/broker-registrar-token.js +215 -0
- package/dist/plan/broker-registrar.d.ts +2 -0
- package/dist/plan/broker-registrar.js +5 -1
- package/dist/plan/diff.d.ts +13 -0
- package/dist/plan/diff.js +26 -5
- package/dist/plan/executor.d.ts +66 -0
- package/dist/plan/executor.js +136 -8
- package/dist/plan/health-check-settings.d.ts +13 -0
- package/dist/plan/health-check-settings.js +17 -0
- package/dist/plan/own-diff.d.ts +32 -0
- package/dist/plan/own-diff.js +67 -10
- package/dist/plan/own-hostnames.d.ts +8 -0
- package/dist/plan/own-hostnames.js +14 -0
- package/dist/plan/own-services.d.ts +7 -0
- package/dist/plan/own-services.js +2 -0
- package/dist/plan/plan-apply.d.ts +15 -0
- package/dist/plan/plan-apply.js +21 -0
- package/dist/plan/platform-diff.d.ts +9 -5
- package/dist/plan/platform-diff.js +16 -10
- package/dist/plan/platform-providers.d.ts +24 -2
- package/dist/plan/platform-providers.js +155 -12
- package/dist/plan/signin-mail.d.ts +18 -5
- package/dist/plan/signin-mail.js +14 -6
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +72 -46
- package/dist/plan/zitadel-machine-user.d.ts +14 -1
- package/dist/plan/zitadel-machine-user.js +96 -13
- package/package.json +3 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
2
|
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
3
3
|
import { AUTH_REGISTRAR_KEY_ENV, AUTH_REGISTRAR_URL_ENV, customersOrgIdOf, registrarWanted, } from './boule.js';
|
|
4
|
+
import { REGISTRAR_TOKEN_ORGS_SCOPE_KEY, registrarOrgsMarker } from './broker-registrar-token.js';
|
|
4
5
|
import { loginHostnameFor } from './login-host.js';
|
|
5
6
|
import { brokerDeployRecordValue, missingBrokerInputs } from './platform-providers.js';
|
|
6
7
|
import { testCustomersOrgKey } from './workspace-client.js';
|
|
@@ -403,7 +404,10 @@ export function planBrokerRegistrar(org, recorded, plannedSteps, host) {
|
|
|
403
404
|
else {
|
|
404
405
|
const mint = recorded[REGISTRAR_MINTED_SCOPE_KEY];
|
|
405
406
|
if (mint &&
|
|
406
|
-
recorded[REGISTRAR_APPLIED_SCOPE_KEY] === registrarAppliedValue(wanted.want, mint)
|
|
407
|
+
recorded[REGISTRAR_APPLIED_SCOPE_KEY] === registrarAppliedValue(wanted.want, mint) &&
|
|
408
|
+
!(host.sharedGrants &&
|
|
409
|
+
recorded[REGISTRAR_TOKEN_ORGS_SCOPE_KEY] !==
|
|
410
|
+
registrarOrgsMarker([wanted.want.organizationId, ...wanted.want.customers]))) {
|
|
407
411
|
return { deliver, keepBouleEnv: false, step: null, blocked };
|
|
408
412
|
}
|
|
409
413
|
}
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Plan, PlanStep, UnmanagedItem, VersionChangeItem } from '../plan.j
|
|
|
3
3
|
import { type AgoraHostReading } from './agora.js';
|
|
4
4
|
import { type BlueGreenServer } from './blue-green.js';
|
|
5
5
|
import { type LiveResource } from './live.js';
|
|
6
|
+
import { type OwnAuthHead } from './own-diff.js';
|
|
6
7
|
import { type OrgPlatformReadiness } from './platform-org.js';
|
|
7
8
|
import { type RemoteDatabaseFacts } from './remote-database.js';
|
|
8
9
|
/**
|
|
@@ -101,8 +102,12 @@ export interface PlanOrgOptions {
|
|
|
101
102
|
ownMailKeyHeld?: boolean;
|
|
102
103
|
/** The executor can mint an own org's Lettermint key (`FoundryStepExecutor.ownMailKeyMintable`). */
|
|
103
104
|
ownMailKeyMintable?: boolean;
|
|
105
|
+
/** The ZITADEL port can set the customers organisation's login policy. Only `false` plans none; unset plans it. */
|
|
106
|
+
customersLoginPolicy?: boolean;
|
|
104
107
|
/** C3: the executor can mint a customers directory credential (`ZitadelPort.mintOrgDirectoryReader`). */
|
|
105
108
|
customersDirectory?: boolean;
|
|
109
|
+
/** The ZITADEL port can grant the directory user its customer removal role (`ZitadelPort.ensureOrgDirectoryRoles`). */
|
|
110
|
+
customersDirectoryRoles?: boolean;
|
|
106
111
|
/** C3: the executor can mint Boule's Archon credential (`FoundryStepExecutorPorts.mintBouleArchonCredential`). */
|
|
107
112
|
bouleArchonCredential?: boolean;
|
|
108
113
|
/** The executor can store a Worker app database's CA and certificate (`FoundryStepExecutor.workerAppDbKeysPuttable`). Absent: true. */
|
|
@@ -113,6 +118,14 @@ export interface PlanOrgOptions {
|
|
|
113
118
|
ownSigninDbKeysPuttable?: boolean;
|
|
114
119
|
/** The executor's company ZITADEL port can write the sign-in mail (`FoundryStepExecutor.ownSigninMailWritable`). Absent: true. */
|
|
115
120
|
ownSigninMailWritable?: boolean;
|
|
121
|
+
/** #886: the head of the declared auth branch (`FoundryStepExecutor.ownAuthHead`). Absent: not read. */
|
|
122
|
+
ownAuthHead?: OwnAuthHead;
|
|
123
|
+
/** The executor's company ZITADEL port can make the broker's registrar token. Absent: not asked for. */
|
|
124
|
+
ownRegistrarTokenWritable?: boolean;
|
|
125
|
+
/** The executor can grant the shared sign-in's registrar user its organisations (`ZitadelPort.ensureRegistrarUser`). Absent: false. */
|
|
126
|
+
registrarGrants?: boolean;
|
|
127
|
+
/** An own org's registrar token is held. Absent: false. */
|
|
128
|
+
ownRegistrarTokenHeld?: boolean;
|
|
116
129
|
/** The host supplies the backup bucket the sign-in database dumps into (`FoundryStepExecutor.ownSigninDbBackupConfigured`). Absent: true. */
|
|
117
130
|
ownSigninDbBackupConfigured?: boolean;
|
|
118
131
|
/** The own jobs admin token's held-key ref holds a value. */
|
package/dist/plan/diff.js
CHANGED
|
@@ -9,7 +9,7 @@ import { planAppsDbRoute } from './apps-db-route.js';
|
|
|
9
9
|
import { APPS_DB_BLOCKED_KEY, planAppsDb } from './apps-db.js';
|
|
10
10
|
import { planAppsZones } from './apps-zones.js';
|
|
11
11
|
import { bgKey, planBlueGreen, } from './blue-green.js';
|
|
12
|
-
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';
|
|
12
|
+
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_DIRECTORY_ROLES_SCOPE_KEY, CUSTOMERS_DIRECTORY_ROLES_VERSION, CUSTOMERS_DIRECTORY_USER_SCOPE_KEY, CUSTOMERS_POLICY_SCOPE_KEY, CUSTOMERS_POLICY_VERSION, 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';
|
|
13
13
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, entryMcpProjectScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
14
14
|
import { bouleRegistrarDrift, planBrokerRegistrar } from './broker-registrar.js';
|
|
15
15
|
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
@@ -265,6 +265,9 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
265
265
|
signinDbKeysPuttable: options.ownSigninDbKeysPuttable ?? true,
|
|
266
266
|
signinDbBackupConfigured: options.ownSigninDbBackupConfigured ?? true,
|
|
267
267
|
signinMailWritable: options.ownSigninMailWritable ?? true,
|
|
268
|
+
...(options.ownAuthHead ? { authHead: options.ownAuthHead } : {}),
|
|
269
|
+
registrarTokenWritable: options.ownRegistrarTokenWritable,
|
|
270
|
+
registrarTokenHeld: options.ownRegistrarTokenHeld ?? false,
|
|
268
271
|
});
|
|
269
272
|
if (ownAuth.step)
|
|
270
273
|
plannedSteps.push(ownAuth.step);
|
|
@@ -470,7 +473,7 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
470
473
|
// this array order, one at a time.
|
|
471
474
|
// Customers before the entries that sign them in: `pushAppEnv` and the
|
|
472
475
|
// broker entry read the customers organisation's id.
|
|
473
|
-
const customers = planCustomers(org, live, claimed, recordedIds, options.zitadelOrgMetadata ?? false);
|
|
476
|
+
const customers = planCustomers(org, live, claimed, recordedIds, options.zitadelOrgMetadata ?? false, options.customersLoginPolicy !== false);
|
|
474
477
|
if (customers.step)
|
|
475
478
|
plannedSteps.push(customers.step);
|
|
476
479
|
// The workspaces' own customers, before the clients and the workspace steps that read the organisation's id.
|
|
@@ -702,6 +705,7 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
702
705
|
const bouleCredentials = planBouleCredentials(org, recordedIds, {
|
|
703
706
|
archon: options.bouleArchonCredential === true,
|
|
704
707
|
directory: options.customersDirectory === true,
|
|
708
|
+
directoryRoles: options.customersDirectoryRoles === true,
|
|
705
709
|
}, platformReadiness.canHoldSecrets === true, (options.now ?? Date.now)());
|
|
706
710
|
// F13: Boule's `CUSTOMER_GRANTS`, from the entries that declare `uses.customerGrants`. Every reason it
|
|
707
711
|
// says nothing yet is a plan line, never silence.
|
|
@@ -723,6 +727,7 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
723
727
|
brokerWired: options.broker ?? false,
|
|
724
728
|
canHoldSecrets: platformReadiness.canHoldSecrets === true,
|
|
725
729
|
issuerKnown: Boolean(authIssuer),
|
|
730
|
+
sharedGrants: !ownsServices(org) && options.registrarGrants === true,
|
|
726
731
|
});
|
|
727
732
|
const boule = planBoule(org, byKey, claimed, recordedIds, authIssuer, options.otherSlugs ?? [], options.archonUrl ?? null, platformReadiness, {
|
|
728
733
|
...bouleCredentials.wants,
|
|
@@ -3361,6 +3366,10 @@ function planBouleCredentials(org, recordedIds, host, canHoldSecrets, nowMs) {
|
|
|
3361
3366
|
wants: {
|
|
3362
3367
|
archonCredential,
|
|
3363
3368
|
customersDirectory,
|
|
3369
|
+
directoryRolesDue: customersDirectory &&
|
|
3370
|
+
host.directoryRoles === true &&
|
|
3371
|
+
Boolean(recordedIds[CUSTOMERS_DIRECTORY_USER_SCOPE_KEY]) &&
|
|
3372
|
+
recordedIds[CUSTOMERS_DIRECTORY_ROLES_SCOPE_KEY] !== CUSTOMERS_DIRECTORY_ROLES_VERSION,
|
|
3364
3373
|
archonRenewDue: archonCredential && archonCredentialRenewDue(recordedIds, nowMs),
|
|
3365
3374
|
},
|
|
3366
3375
|
blocked,
|
|
@@ -3479,6 +3488,10 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
3479
3488
|
!liveEnv[BOULE_CUSTOMERS_CREDENTIAL_ENV]) {
|
|
3480
3489
|
drift.push(BOULE_CUSTOMERS_CREDENTIAL_ENV);
|
|
3481
3490
|
}
|
|
3491
|
+
// The directory user that predates the customer removal role: the step grants it once.
|
|
3492
|
+
if (credentials.directoryRolesDue && !drift.includes(BOULE_CUSTOMERS_CREDENTIAL_ENV)) {
|
|
3493
|
+
drift.push(BOULE_CUSTOMERS_CREDENTIAL_ENV);
|
|
3494
|
+
}
|
|
3482
3495
|
for (const key of staleCustomersEnv(org, liveEnv)) {
|
|
3483
3496
|
if (!drift.includes(key))
|
|
3484
3497
|
drift.push(key);
|
|
@@ -4721,7 +4734,7 @@ function planBouleBrokerEntry(org, recordedIds, plannedSteps, brokerWired) {
|
|
|
4721
4734
|
* recorded, the live project grant to that org is claimed so it is not
|
|
4722
4735
|
* listed as unmanaged.
|
|
4723
4736
|
*/
|
|
4724
|
-
function planCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
4737
|
+
function planCustomers(org, live, claimed, recordedIds, metadataWired, policyAvailable = true) {
|
|
4725
4738
|
const signUp = org.customers?.signUp === true;
|
|
4726
4739
|
const wanted = org.customers?.enabled === true ||
|
|
4727
4740
|
org.hosting.entries.some((entry) => entry.uses?.auth?.users === 'customers');
|
|
@@ -4776,8 +4789,14 @@ function planCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
|
4776
4789
|
blocked.push(signInMetadataBlocked(org));
|
|
4777
4790
|
const writeSignIn = signInDiffers && metadataWired;
|
|
4778
4791
|
const writeRoom = roomDiffers && metadataWired;
|
|
4779
|
-
|
|
4792
|
+
// The login policy needs no metadata write, so it is never blocked: a stack plans it once. A host whose
|
|
4793
|
+
// port cannot write it plans none, and no marker is owed.
|
|
4794
|
+
const writePolicy = wanted &&
|
|
4795
|
+
policyAvailable &&
|
|
4796
|
+
recordedIds[CUSTOMERS_POLICY_SCOPE_KEY] !== CUSTOMERS_POLICY_VERSION;
|
|
4797
|
+
if (applied === wantedSignUp && !writeSignIn && !writeRoom && !writePolicy) {
|
|
4780
4798
|
return { step: null, blocked };
|
|
4799
|
+
}
|
|
4781
4800
|
return {
|
|
4782
4801
|
step: {
|
|
4783
4802
|
action: {
|
|
@@ -4794,7 +4813,9 @@ function planCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
|
4794
4813
|
description: applied === wantedSignUp
|
|
4795
4814
|
? writeSignIn
|
|
4796
4815
|
? `${wantedSignIn ? 'set' : 'remove'} the emailed-code sign-in metadata of ${org.slug}'s customers`
|
|
4797
|
-
:
|
|
4816
|
+
: writeRoom
|
|
4817
|
+
? `${wantedRoom ? 'set' : 'remove'} the sign-in window text of ${org.slug}'s customers`
|
|
4818
|
+
: `set the login policy of ${org.slug}'s customers: every sign-in method and second step an app can choose`
|
|
4798
4819
|
: `turn self-registration ${wantedSignUp === '1' ? 'on' : 'off'} for ${org.slug}'s customers`,
|
|
4799
4820
|
destroys: false,
|
|
4800
4821
|
migrates: false,
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { GithubDeployKeysPort } from './github-deploy-keys.js';
|
|
|
17
17
|
import { type KeysRegistrarPort } from './keys-registrar.js';
|
|
18
18
|
import { type LoginHostBindingsPort } from './login-host.js';
|
|
19
19
|
import { type MailCopyPort } from './mail-copy.js';
|
|
20
|
+
import { type OwnAuthHead } from './own-diff.js';
|
|
20
21
|
import { type OwnServicesPort } from './own-services.js';
|
|
21
22
|
import type { DestroyTarget, StepDestroyer, StepExecutionContext, StepExecutor } from './plan-apply.js';
|
|
22
23
|
import type { EdgePortName } from './platform-org.js';
|
|
@@ -124,6 +125,8 @@ export interface CoolifyPort {
|
|
|
124
125
|
/** #788: where Coolify reports previews is not documented, so `previewsOn` reads both places. */
|
|
125
126
|
is_preview_deployments_enabled?: boolean;
|
|
126
127
|
custom_network_aliases?: string | null;
|
|
128
|
+
/** A fixed container name: while it is set Coolify runs no rolling update (`rollingUpdateSettings` clears it). */
|
|
129
|
+
custom_internal_name?: string | null;
|
|
127
130
|
health_check_enabled?: boolean;
|
|
128
131
|
ports_exposes?: string;
|
|
129
132
|
/** Coolify's own record of the resource limits (`plan/own-limits.ts`). */
|
|
@@ -1029,6 +1032,17 @@ export interface ZitadelPort {
|
|
|
1029
1032
|
enforceSecondFactor(orgId: string): Promise<{
|
|
1030
1033
|
changed: boolean;
|
|
1031
1034
|
}>;
|
|
1035
|
+
/**
|
|
1036
|
+
* Gives the customers org one fixed, permissive login policy that every customer app can narrow: username
|
|
1037
|
+
* and password allowed, passkeys allowed, no forced second factor, no external IdP, an authenticator app and
|
|
1038
|
+
* a security key as second factors, a user-verified passkey as the multi factor. `allowRegister` is left as
|
|
1039
|
+
* it is (`allowSelfRegistration` and `closeSelfRegistration` own it). An org on the instance default gets
|
|
1040
|
+
* its own policy. Idempotent: a policy already so set is no write. `zitadel-login-policy.ts` is the real
|
|
1041
|
+
* implementation. Optional: a host without it plans no policy (`customersLoginPolicyAvailable`).
|
|
1042
|
+
*/
|
|
1043
|
+
setCustomersLoginPolicy?(orgId: string): Promise<{
|
|
1044
|
+
changed: boolean;
|
|
1045
|
+
}>;
|
|
1032
1046
|
/** Grants another ZITADEL org access to a project (plan gap A9). Idempotent. */
|
|
1033
1047
|
findOrCreateProjectGrant(orgId: string, projectId: string, grantedOrgId: string): Promise<{
|
|
1034
1048
|
created: boolean;
|
|
@@ -1156,6 +1170,33 @@ export interface ZitadelPort {
|
|
|
1156
1170
|
token: string;
|
|
1157
1171
|
patId: string;
|
|
1158
1172
|
}>;
|
|
1173
|
+
/**
|
|
1174
|
+
* The sign-in broker's registrar user (`ZITADEL_REGISTRAR_TOKEN`, auth ADR 0003): a machine user of its
|
|
1175
|
+
* own in `orgIds[0]` with the project-creator role in every org of `orgIds`. Idempotent, mints no token,
|
|
1176
|
+
* so it is repeated whenever an org joins. Optional: a host without it gets the registrar token reported
|
|
1177
|
+
* blocked. `zitadel-machine-user.ts` is the real implementation.
|
|
1178
|
+
*/
|
|
1179
|
+
ensureRegistrarUser?(orgIds: readonly string[], input: {
|
|
1180
|
+
userName: string;
|
|
1181
|
+
name: string;
|
|
1182
|
+
}): Promise<{
|
|
1183
|
+
userId: string;
|
|
1184
|
+
}>;
|
|
1185
|
+
/**
|
|
1186
|
+
* A fresh token for the registrar user, made in its home org (`orgIds[0]` above); every token it holds is
|
|
1187
|
+
* revoked first. The caller holds the token and writes it to the broker's env; it never reaches an error,
|
|
1188
|
+
* a log or a recorded id.
|
|
1189
|
+
*/
|
|
1190
|
+
mintRegistrarToken?(homeOrgId: string, userId: string): Promise<{
|
|
1191
|
+
token: string;
|
|
1192
|
+
patId: string;
|
|
1193
|
+
}>;
|
|
1194
|
+
/**
|
|
1195
|
+
* Grants the directory user the read role and the user-management role in that one organisation, replacing
|
|
1196
|
+
* the roles it holds there; a user that is not a member yet is added. Idempotent. Optional: a host without
|
|
1197
|
+
* it leaves the directory user read-only, and Boule cannot remove a customer.
|
|
1198
|
+
*/
|
|
1199
|
+
ensureOrgDirectoryRoles?(orgId: string, userId: string): Promise<void>;
|
|
1159
1200
|
/** C3: revokes every PAT of the directory reader, so a company that turns customers off leaves no live credential. A PAT already gone is not an error. */
|
|
1160
1201
|
revokeOrgDirectoryReader?(orgId: string, userId: string): Promise<void>;
|
|
1161
1202
|
/**
|
|
@@ -2092,8 +2133,16 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2092
2133
|
private executeScoped;
|
|
2093
2134
|
/** Whether `ensureOwnAuthMailToken` can mint: the same two inputs it blocks on. */
|
|
2094
2135
|
ownMailKeyMintable(): boolean;
|
|
2136
|
+
/** Whether the ZITADEL port can grant the shared sign-in's registrar user a company's organisations. */
|
|
2137
|
+
registrarGrantsAvailable(): boolean;
|
|
2138
|
+
/** Whether the host's company ZITADEL port can make the broker's registrar token (`provision.auth.registrarTokenWritable`). */
|
|
2139
|
+
ownRegistrarTokenWritable(): boolean;
|
|
2140
|
+
/** Whether the ZITADEL port can set the customers organisation's login policy. Without it no policy is planned. */
|
|
2141
|
+
customersLoginPolicyAvailable(): boolean;
|
|
2095
2142
|
/** C3: whether the ZITADEL port can mint and revoke a customers directory credential. */
|
|
2096
2143
|
customersDirectoryAvailable(): boolean;
|
|
2144
|
+
/** Whether the ZITADEL port can grant the directory user its customer removal role. */
|
|
2145
|
+
customersDirectoryRolesAvailable(): boolean;
|
|
2097
2146
|
/** C3: whether the host supplied `mintBouleArchonCredential`. */
|
|
2098
2147
|
bouleArchonCredentialAvailable(): boolean;
|
|
2099
2148
|
/** Whether the held keys can store a new company's sign-in database CA and certificate (#746). */
|
|
@@ -2103,6 +2152,12 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2103
2152
|
* (`provision.auth.signinMailWritable`). The port is a lazy proxy that answers any name, so it cannot be probed.
|
|
2104
2153
|
*/
|
|
2105
2154
|
ownSigninMailWritable(): boolean;
|
|
2155
|
+
/**
|
|
2156
|
+
* #886: the head of the declared auth branch, read through the host's `ownServices.readBranchHead` under a
|
|
2157
|
+
* timeout. `undefined` when there is nothing to read: the ref is a tag or a commit (it names one build), or the
|
|
2158
|
+
* host supplies no read. A failed or malformed read is a fixed reason, never the cause (it can echo a credential).
|
|
2159
|
+
*/
|
|
2160
|
+
ownAuthHead(gitRef: string): Promise<OwnAuthHead | undefined>;
|
|
2106
2161
|
/** Whether the held keys can store a Worker app database's CA and certificate. */
|
|
2107
2162
|
workerAppDbKeysPuttable(): boolean;
|
|
2108
2163
|
/** Whether the host supplies the backup bucket a Worker app database dumps into (the files host's bucket and key ports). */
|
|
@@ -3513,6 +3568,11 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
3513
3568
|
* leaves one live token. `null` while the customers organisation is not recorded yet.
|
|
3514
3569
|
*/
|
|
3515
3570
|
private ensureCustomersDirectoryCredential;
|
|
3571
|
+
/**
|
|
3572
|
+
* A directory user made before it could remove customers gets that role in the customers organisation
|
|
3573
|
+
* (and in no other), once: the grant is idempotent and the marker is recorded after it.
|
|
3574
|
+
*/
|
|
3575
|
+
private ensureDirectoryRoles;
|
|
3516
3576
|
/** The held keys with a timeout on every call: a hung store stops the step with a plain line. */
|
|
3517
3577
|
private timedHeldKeys;
|
|
3518
3578
|
/** T3: the company's registrar key, held at `foundry:registrar:<org id>`; either step may make it. */
|
|
@@ -3529,6 +3589,12 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
3529
3589
|
* last and never holds the key; every error is redacted of it.
|
|
3530
3590
|
*/
|
|
3531
3591
|
private brokerRegistrar;
|
|
3592
|
+
/**
|
|
3593
|
+
* A company on the shared sign-in: the platform broker's registrar user holds the project-creator role in
|
|
3594
|
+
* the company's sign-in and customers organisations, so the broker can register its apps there. Repeated
|
|
3595
|
+
* whenever the organisations change; no token is made here (the platform's sign-in step makes it).
|
|
3596
|
+
*/
|
|
3597
|
+
private grantSharedRegistrar;
|
|
3532
3598
|
private customerGrantsCredential;
|
|
3533
3599
|
/**
|
|
3534
3600
|
* F13, app side: an entry with `uses.customerGrants` gets `CUSTOMER_GRANTS_CREDENTIAL` (made and held first,
|
package/dist/plan/executor.js
CHANGED
|
@@ -17,11 +17,12 @@ import { APPS_ZONES_APPLIED_SCOPE_KEY, applyAppsZones, retireAppsZones } from '.
|
|
|
17
17
|
import { applyBlueGreenConnectors, applyBlueGreenServer, applyBlueGreenWeights, applyWorkspaceServer, } from './blue-green-apply.js';
|
|
18
18
|
import { actionsKeyName, bgKey, blueGreenSubjects, colorAppName, healthTokenName, liveAppNameFor, mintActionsKey, mintPinKey, parseGuards, pinKeyName, splitServers, } from './blue-green.js';
|
|
19
19
|
import { BouleOwnersError, httpBouleOwners } from './boule-owners-http.js';
|
|
20
|
-
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, BOULE_TEST_CUSTOMERS_ORG_ENV, 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';
|
|
20
|
+
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, BOULE_TEST_CUSTOMERS_ORG_ENV, CUSTOMERS_DIRECTORY_ROLES_SCOPE_KEY, CUSTOMERS_DIRECTORY_ROLES_VERSION, CUSTOMERS_DIRECTORY_USER_SCOPE_KEY, CUSTOMERS_POLICY_SCOPE_KEY, CUSTOMERS_POLICY_VERSION, 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';
|
|
21
21
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, entryMcp, entryMcpProjectScopeKey, mcpCallbackUri, removeBrokerEntry, wantedBrokerEntry, writeBrokerEntry, } from './broker-entries.js';
|
|
22
|
+
import { REGISTRAR_TOKEN_ORGS_SCOPE_KEY, REGISTRAR_TOKEN_USER_NAME, registrarOrgIdsOf, registrarOrgsMarker, } from './broker-registrar-token.js';
|
|
22
23
|
import { AUTH_REGISTRAR_KEY_ENV, AUTH_REGISTRAR_URL_ENV, REGISTRAR_APPLIED_SCOPE_KEY, REGISTRAR_DELIVERED_SCOPE_KEY, REGISTRAR_DEPLOY_WAITS_SCOPE_KEY, REGISTRAR_MINTED_SCOPE_KEY, REGISTRAR_REMOVAL_HELD_SCOPE_KEY, appliedRegistrarOrganization, ensureRegistrarKey, registrarAppliedValue, registrarRef, registrarWanted, wantedRegistrar, writeRegistrar, } from './broker-registrar.js';
|
|
23
24
|
import { BROKER_WORKSPACES_DATABASE_SCOPE_KEY, canHoldWorkspacesDatabase, ensureBrokerWorkspacesDatabase, } from './broker-workspaces-database.js';
|
|
24
|
-
import { recordBuiltCommit } from './build-record.js';
|
|
25
|
+
import { builtCommitScopeKey, recordBuiltCommit } from './build-record.js';
|
|
25
26
|
import { CallError, withTimeout as withLabelledTimeout } from './call-timeout.js';
|
|
26
27
|
import { MAS_TAG, SYNAPSE_IMAGE, SYNAPSE_TAG, callsMasConfigYaml, callsSynapseHomeserverYaml, masDockerfile, mintCallsMatrixSecrets, parseCallsDbUrl, } from './calls-matrix.js';
|
|
27
28
|
import { CHAT_MAS_PORT, CHAT_REPO, CHAT_SERVICE_DOCKERFILE, CHAT_SERVICE_PORT, CHAT_SYNAPSE_DOCKERFILE, CHAT_SYNAPSE_PORT, assertChatServerName, chatInternalNames, chatMasConfigYaml, chatRekeyedInternalNames, chatServerName, chatServiceEnvs, chatSynapseHomeserverYaml, chatUpstreams, } from './chat-matrix.js';
|
|
@@ -66,15 +67,15 @@ import { NETWORK_APPLIED_SCOPE_KEY } from './network-move.js';
|
|
|
66
67
|
import { provisionOrgAppsAdmin, retireOrgAppsAdmin, } from './org-apps-admin.js';
|
|
67
68
|
import { ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationNotConfirmedError, migrateHomeOf, migrateOrgPostgresHome, parseMigrationConfirmation, } from './org-postgres-migrate.js';
|
|
68
69
|
import { orgPostgresSecretRef, provisionOrgPostgresDatabases, provisionOrgPostgresServer, serviceEndpoint, serviceOnServer, serviceUrls, } from './org-postgres.js';
|
|
69
|
-
import { OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY, OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, } from './own-diff.js';
|
|
70
|
-
import { NO_DOMAIN_ZONE, ownHostnames } from './own-hostnames.js';
|
|
70
|
+
import { OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY, OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, OWN_AUTH_REPOSITORY, isBranchRef, ownAuthBuildApps, } from './own-diff.js';
|
|
71
|
+
import { NO_DOMAIN_ZONE, ownHostnames, passkeyRpIdFor } from './own-hostnames.js';
|
|
71
72
|
import { provisionJobsRoute } from './own-jobs-route.js';
|
|
72
73
|
import { LIMITS, limited } from './own-limits.js';
|
|
73
74
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgNetwork, orgTarget, platformTarget, scopeFromOrg, } from './own-services.js';
|
|
74
75
|
import { ownSfuUrls, provisionSfuApp } from './own-sfu.js';
|
|
75
76
|
import { JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, } from './platform-diff.js';
|
|
76
77
|
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, appImageAppliedScopeKey, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, entryImageFields, entryImageRef, orgDeclaresMcp, resolveAppEnv, serviceDbEnvAppliedScopeKey, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
77
|
-
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_AUTH_PROJECT, PLATFORM_MAIL_SERVER_SCOPE_KEY, defaultStalwartForInstance, filesDepsInvalid, filesShareDomains, provisionAuthStack, provisionDatabaseBackup, provisionFilesHost, provisionJobsHost, provisionMailServer, resolveOwnFilesKeys, } from './platform-providers.js';
|
|
78
|
+
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_AUTH_ORG_NAME, PLATFORM_AUTH_PROJECT, PLATFORM_MAIL_SERVER_SCOPE_KEY, defaultStalwartForInstance, filesDepsInvalid, filesShareDomains, provisionAuthStack, provisionDatabaseBackup, provisionFilesHost, provisionJobsHost, provisionMailServer, resolveOwnFilesKeys, } from './platform-providers.js';
|
|
78
79
|
import { companyAppPostLogoutUris, postLogoutAppliedValue, postLogoutUrisFor, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
79
80
|
import { CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY, CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, ENTRY_CONTAINER_LABELS, appLabelsAppliedScopeKey, builtinRoutes, entryAppsHostnames, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryProxyLabels, entryRoutes, entryRoutesMarker, ownsAppsZoneName, ownsOikosName, routesWithoutToken, sameLabelSets, } from './proxy-labels.js';
|
|
80
81
|
import { STAGE_DEPLOY_ATTEMPTS, STAGE_DEPLOY_INTERVAL_MS, createReleaseHost, } from './release-host.js';
|
|
@@ -146,6 +147,8 @@ const BOULE_CREDENTIAL_TIMEOUT_MS = 60_000;
|
|
|
146
147
|
/** One read of the broker's env. */
|
|
147
148
|
const BROKER_ENV_READ_TIMEOUT_MS = 15_000;
|
|
148
149
|
const HELD_KEY_READ_TIMEOUT_MS = 15_000;
|
|
150
|
+
/** One ZITADEL call that grants the shared registrar user its organisations. */
|
|
151
|
+
const REGISTRAR_CALL_TIMEOUT_MS = 30_000;
|
|
149
152
|
const BOULE_CREDENTIAL_ATTEMPTS = 3;
|
|
150
153
|
/** The ZITADEL machine user that reads one company's customers organisation. */
|
|
151
154
|
const CUSTOMERS_DIRECTORY_USER_NAME = 'foundry-customers-directory';
|
|
@@ -922,6 +925,8 @@ async function withTimeout(work, ms) {
|
|
|
922
925
|
clearTimeout(timer);
|
|
923
926
|
}
|
|
924
927
|
}
|
|
928
|
+
/** How long the host may take to read the head of the auth branch (#886). */
|
|
929
|
+
const AUTH_HEAD_READ_TIMEOUT_MS = 15_000;
|
|
925
930
|
/** How long one call to the jobs scheduler or the held keys, made for an entry's project, may take. */
|
|
926
931
|
const JOBS_PROJECT_CALL_TIMEOUT_MS = 30_000;
|
|
927
932
|
/** How many times an entry's project credentials are tried against the held keys. */
|
|
@@ -1081,11 +1086,27 @@ export class FoundryStepExecutor {
|
|
|
1081
1086
|
ownMailKeyMintable() {
|
|
1082
1087
|
return Boolean(this.ports.lettermint && this.ports.ownServices?.provision.auth.heldKeys);
|
|
1083
1088
|
}
|
|
1089
|
+
/** Whether the ZITADEL port can grant the shared sign-in's registrar user a company's organisations. */
|
|
1090
|
+
registrarGrantsAvailable() {
|
|
1091
|
+
return typeof this.ports.zitadel.ensureRegistrarUser === 'function';
|
|
1092
|
+
}
|
|
1093
|
+
/** Whether the host's company ZITADEL port can make the broker's registrar token (`provision.auth.registrarTokenWritable`). */
|
|
1094
|
+
ownRegistrarTokenWritable() {
|
|
1095
|
+
return this.ports.ownServices?.provision.auth.registrarTokenWritable === true;
|
|
1096
|
+
}
|
|
1097
|
+
/** Whether the ZITADEL port can set the customers organisation's login policy. Without it no policy is planned. */
|
|
1098
|
+
customersLoginPolicyAvailable() {
|
|
1099
|
+
return typeof this.ports.zitadel.setCustomersLoginPolicy === 'function';
|
|
1100
|
+
}
|
|
1084
1101
|
/** C3: whether the ZITADEL port can mint and revoke a customers directory credential. */
|
|
1085
1102
|
customersDirectoryAvailable() {
|
|
1086
1103
|
return (typeof this.ports.zitadel.mintOrgDirectoryReader === 'function' &&
|
|
1087
1104
|
typeof this.ports.zitadel.revokeOrgDirectoryReader === 'function');
|
|
1088
1105
|
}
|
|
1106
|
+
/** Whether the ZITADEL port can grant the directory user its customer removal role. */
|
|
1107
|
+
customersDirectoryRolesAvailable() {
|
|
1108
|
+
return typeof this.ports.zitadel.ensureOrgDirectoryRoles === 'function';
|
|
1109
|
+
}
|
|
1089
1110
|
/** C3: whether the host supplied `mintBouleArchonCredential`. */
|
|
1090
1111
|
bouleArchonCredentialAvailable() {
|
|
1091
1112
|
return typeof this.ports.mintBouleArchonCredential === 'function';
|
|
@@ -1101,6 +1122,30 @@ export class FoundryStepExecutor {
|
|
|
1101
1122
|
ownSigninMailWritable() {
|
|
1102
1123
|
return this.ports.ownServices?.provision.auth.signinMailWritable === true;
|
|
1103
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* #886: the head of the declared auth branch, read through the host's `ownServices.readBranchHead` under a
|
|
1127
|
+
* timeout. `undefined` when there is nothing to read: the ref is a tag or a commit (it names one build), or the
|
|
1128
|
+
* host supplies no read. A failed or malformed read is a fixed reason, never the cause (it can echo a credential).
|
|
1129
|
+
*/
|
|
1130
|
+
async ownAuthHead(gitRef) {
|
|
1131
|
+
const read = this.ports.ownServices?.readBranchHead?.bind(this.ports.ownServices);
|
|
1132
|
+
if (!read || !isBranchRef(gitRef))
|
|
1133
|
+
return undefined;
|
|
1134
|
+
let commit;
|
|
1135
|
+
try {
|
|
1136
|
+
commit = await withLabelledTimeout('auth branch head', AUTH_HEAD_READ_TIMEOUT_MS, () => read(OWN_AUTH_REPOSITORY, gitRef));
|
|
1137
|
+
}
|
|
1138
|
+
catch (error) {
|
|
1139
|
+
return {
|
|
1140
|
+
failed: error instanceof CallError && error.message.includes('no answer')
|
|
1141
|
+
? 'the auth branch head read timed out'
|
|
1142
|
+
: 'the auth branch head read failed',
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
return /^[0-9a-f]{40}$/.test(commit)
|
|
1146
|
+
? { commit }
|
|
1147
|
+
: { failed: 'the auth branch head was not a commit' };
|
|
1148
|
+
}
|
|
1104
1149
|
/** Whether the held keys can store a Worker app database's CA and certificate. */
|
|
1105
1150
|
workerAppDbKeysPuttable() {
|
|
1106
1151
|
return typeof this.ports.heldKeys?.put === 'function';
|
|
@@ -2756,6 +2801,11 @@ export class FoundryStepExecutor {
|
|
|
2756
2801
|
await this.ports.zitadel.allowSelfRegistration(customers.id);
|
|
2757
2802
|
else
|
|
2758
2803
|
await this.ports.zitadel.closeSelfRegistration(customers.id);
|
|
2804
|
+
// The live customers org only: the workspaces' test customers keep the instance policy. Read after the
|
|
2805
|
+
// sign-up call, so the policy keeps `allowRegister` as that call left it.
|
|
2806
|
+
const policyWritten = variant === LIVE_CUSTOMERS && this.customersLoginPolicyAvailable()
|
|
2807
|
+
? await this.ports.zitadel.setCustomersLoginPolicy?.(customers.id)
|
|
2808
|
+
: undefined;
|
|
2759
2809
|
await this.ports.zitadel.findOrCreateProjectGrant(zitadelOrgId, projectId, customers.id);
|
|
2760
2810
|
// The sign-in and room metadata come before every marker: a failed write leaves both markers behind, so the next plan repeats the step.
|
|
2761
2811
|
const signIn = await this.writeCustomersSignInMetadata(org, customers.id, action, context, variant);
|
|
@@ -2768,6 +2818,9 @@ export class FoundryStepExecutor {
|
|
|
2768
2818
|
context.recordScopeId(variant.signInKey, signIn);
|
|
2769
2819
|
if (room !== null)
|
|
2770
2820
|
context.recordScopeId(CUSTOMERS_ROOM_SCOPE_KEY, room);
|
|
2821
|
+
if (policyWritten) {
|
|
2822
|
+
context.recordScopeId(CUSTOMERS_POLICY_SCOPE_KEY, CUSTOMERS_POLICY_VERSION);
|
|
2823
|
+
}
|
|
2771
2824
|
return { resourceId: customers.id };
|
|
2772
2825
|
}
|
|
2773
2826
|
/**
|
|
@@ -8397,20 +8450,51 @@ export class FoundryStepExecutor {
|
|
|
8397
8450
|
const put = heldKeys.put;
|
|
8398
8451
|
const ref = customersDirectoryRef(org.id);
|
|
8399
8452
|
const held = await withCallTimeout(BOULE_CREDENTIAL_TIMEOUT_MS, `${at}: read the held directory credential`, () => heldKeys.resolve(ref));
|
|
8400
|
-
if (held)
|
|
8453
|
+
if (held) {
|
|
8454
|
+
await this.ensureDirectoryRoles(org, customersOrgId, context);
|
|
8401
8455
|
return held;
|
|
8456
|
+
}
|
|
8402
8457
|
const minted = await withCallTimeout(BOULE_CREDENTIAL_TIMEOUT_MS, `${at}: mint the directory credential`, () => zitadel.mintOrgDirectoryReader(customersOrgId, {
|
|
8403
8458
|
userName: CUSTOMERS_DIRECTORY_USER_NAME,
|
|
8404
|
-
name: 'Foundry customers directory
|
|
8459
|
+
name: 'Foundry customers directory',
|
|
8405
8460
|
}));
|
|
8406
8461
|
this.handledSecrets.add(minted.token);
|
|
8407
8462
|
context.recordScopeId(CUSTOMERS_DIRECTORY_USER_SCOPE_KEY, minted.userId);
|
|
8463
|
+
// The mint grants the same roles, so a fresh user needs no second grant.
|
|
8464
|
+
if (zitadel.ensureOrgDirectoryRoles) {
|
|
8465
|
+
context.recordScopeId(CUSTOMERS_DIRECTORY_ROLES_SCOPE_KEY, CUSTOMERS_DIRECTORY_ROLES_VERSION);
|
|
8466
|
+
}
|
|
8408
8467
|
await context.persistScopeIds();
|
|
8409
8468
|
if (!(await this.holdWithRetry(ref, minted.token, put, `${at}: hold the directory credential`))) {
|
|
8410
8469
|
throw new Error(`${at}: the directory credential of ZITADEL user ${minted.userId} could not be held after ${BOULE_CREDENTIAL_ATTEMPTS} attempts; the next apply mints it again and revokes this one`);
|
|
8411
8470
|
}
|
|
8412
8471
|
return minted.token;
|
|
8413
8472
|
}
|
|
8473
|
+
/**
|
|
8474
|
+
* A directory user made before it could remove customers gets that role in the customers organisation
|
|
8475
|
+
* (and in no other), once: the grant is idempotent and the marker is recorded after it.
|
|
8476
|
+
*/
|
|
8477
|
+
async ensureDirectoryRoles(org, customersOrgId, context) {
|
|
8478
|
+
const recorded = context.recordedIds();
|
|
8479
|
+
const userId = recorded[CUSTOMERS_DIRECTORY_USER_SCOPE_KEY];
|
|
8480
|
+
const grant = this.ports.zitadel.ensureOrgDirectoryRoles;
|
|
8481
|
+
if (!userId ||
|
|
8482
|
+
!grant ||
|
|
8483
|
+
recorded[CUSTOMERS_DIRECTORY_ROLES_SCOPE_KEY] === CUSTOMERS_DIRECTORY_ROLES_VERSION) {
|
|
8484
|
+
return;
|
|
8485
|
+
}
|
|
8486
|
+
const at = `coolify:boule for ${org.slug}`;
|
|
8487
|
+
try {
|
|
8488
|
+
await withCallTimeout(BOULE_CREDENTIAL_TIMEOUT_MS, `${at}: grant the directory user`, () => grant.call(this.ports.zitadel, customersOrgId, userId));
|
|
8489
|
+
}
|
|
8490
|
+
catch (error) {
|
|
8491
|
+
// The HTTP status says why (403 is a missing right); the body is dropped.
|
|
8492
|
+
const status = error?.status;
|
|
8493
|
+
throw new Error(`${at}: ZITADEL user ${userId} could not be granted its customer removal role${typeof status === 'number' ? ` (HTTP ${status})` : ''}; apply again`);
|
|
8494
|
+
}
|
|
8495
|
+
context.recordScopeId(CUSTOMERS_DIRECTORY_ROLES_SCOPE_KEY, CUSTOMERS_DIRECTORY_ROLES_VERSION);
|
|
8496
|
+
await context.persistScopeIds();
|
|
8497
|
+
}
|
|
8414
8498
|
/** The held keys with a timeout on every call: a hung store stops the step with a plain line. */
|
|
8415
8499
|
timedHeldKeys() {
|
|
8416
8500
|
const held = this.ports.heldKeys;
|
|
@@ -8559,11 +8643,39 @@ export class FoundryStepExecutor {
|
|
|
8559
8643
|
catch (error) {
|
|
8560
8644
|
throw new Error(redactSecrets(error instanceof Error ? error.message : String(error), keys));
|
|
8561
8645
|
}
|
|
8646
|
+
await this.grantSharedRegistrar(org, wanted.want, context);
|
|
8562
8647
|
context.recordScopeId(REGISTRAR_DEPLOY_WAITS_SCOPE_KEY, result.waits.join(', '));
|
|
8563
8648
|
// Last: a run that failed above plans the step again.
|
|
8564
8649
|
context.recordScopeId(REGISTRAR_APPLIED_SCOPE_KEY, registrarAppliedValue(wanted.want, mint));
|
|
8565
8650
|
return {};
|
|
8566
8651
|
}
|
|
8652
|
+
/**
|
|
8653
|
+
* A company on the shared sign-in: the platform broker's registrar user holds the project-creator role in
|
|
8654
|
+
* the company's sign-in and customers organisations, so the broker can register its apps there. Repeated
|
|
8655
|
+
* whenever the organisations change; no token is made here (the platform's sign-in step makes it).
|
|
8656
|
+
*/
|
|
8657
|
+
async grantSharedRegistrar(org, want, context) {
|
|
8658
|
+
const zitadel = this.ports.zitadel;
|
|
8659
|
+
if (ownsServices(org) || typeof zitadel.ensureRegistrarUser !== 'function')
|
|
8660
|
+
return;
|
|
8661
|
+
const covered = [want.organizationId, ...want.customers];
|
|
8662
|
+
const marker = registrarOrgsMarker(covered);
|
|
8663
|
+
if (context.recordedIds()[REGISTRAR_TOKEN_ORGS_SCOPE_KEY] === marker)
|
|
8664
|
+
return;
|
|
8665
|
+
const at = `coolify:broker-registrar for ${org.slug}`;
|
|
8666
|
+
try {
|
|
8667
|
+
const home = (await withCallTimeout(REGISTRAR_CALL_TIMEOUT_MS, `${at}: find the first organisation`, () => zitadel.findOrCreateOrg(PLATFORM_AUTH_ORG_NAME))).org.id;
|
|
8668
|
+
await withCallTimeout(REGISTRAR_CALL_TIMEOUT_MS, `${at}: grant the registrar user`, () => zitadel.ensureRegistrarUser([home, ...covered], {
|
|
8669
|
+
userName: REGISTRAR_TOKEN_USER_NAME,
|
|
8670
|
+
name: 'Foundry (sign-in broker registrar)',
|
|
8671
|
+
}));
|
|
8672
|
+
}
|
|
8673
|
+
catch {
|
|
8674
|
+
// The cause is dropped: nothing here is a secret, but ZITADEL's error can echo a request.
|
|
8675
|
+
throw new Error(`coolify:broker-registrar for ${org.slug}: the registrar user could not be granted the company's organisations; apply again`);
|
|
8676
|
+
}
|
|
8677
|
+
context.recordScopeId(REGISTRAR_TOKEN_ORGS_SCOPE_KEY, marker);
|
|
8678
|
+
}
|
|
8567
8679
|
customerGrantsCredential(org, entryKey, context) {
|
|
8568
8680
|
return ensureCustomerGrantsCredential({
|
|
8569
8681
|
orgId: org.id,
|
|
@@ -9204,8 +9316,15 @@ export class FoundryStepExecutor {
|
|
|
9204
9316
|
const before = scope.recordedIds();
|
|
9205
9317
|
const refChanged = before[OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY] !== undefined &&
|
|
9206
9318
|
before[OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY] !== gitRef;
|
|
9319
|
+
// #886: the branch head the plan read. Validated here: it decides which build is recorded.
|
|
9320
|
+
const head = typeof action.desired.head === 'string' && /^[0-9a-f]{40}$/.test(action.desired.head)
|
|
9321
|
+
? action.desired.head
|
|
9322
|
+
: undefined;
|
|
9323
|
+
const headMoved = head !== undefined &&
|
|
9324
|
+
ownAuthBuildApps(org, before).some((app) => before[builtCommitScopeKey(app.name)] !== head);
|
|
9207
9325
|
const gitApps = [
|
|
9208
9326
|
before[PLATFORM_AUTH_LOGIN_SCOPE_KEY],
|
|
9327
|
+
before[SIGN_IN_LOGIN_APP_SCOPE_KEY],
|
|
9209
9328
|
before[PLATFORM_AUTH_BROKER_SCOPE_KEY],
|
|
9210
9329
|
].filter((id) => Boolean(id));
|
|
9211
9330
|
if (refChanged) {
|
|
@@ -9225,6 +9344,7 @@ export class FoundryStepExecutor {
|
|
|
9225
9344
|
zones: this.zonePort(),
|
|
9226
9345
|
}, scope, `${org.slug}:auth`),
|
|
9227
9346
|
smtpSender: `noreply@${hn.mailDomain}`,
|
|
9347
|
+
...(passkeyRpIdFor(org) ? { passkeyRpId: passkeyRpIdFor(org) } : {}),
|
|
9228
9348
|
defaultRedirectUri: `https://${agoraHostnameFor(org)}`,
|
|
9229
9349
|
ownerCompany: null,
|
|
9230
9350
|
// In-app sign-in: the company's own stack gets the second login app. Its host table starts
|
|
@@ -9232,15 +9352,23 @@ export class FoundryStepExecutor {
|
|
|
9232
9352
|
...(inAppSignIn(org) ? { signInLogin: { bindings: null } } : {}),
|
|
9233
9353
|
mcp: orgDeclaresMcp(org),
|
|
9234
9354
|
mailBranding: { companyName: org.displayName },
|
|
9355
|
+
...(registrarWanted(org) && this.ownRegistrarTokenWritable()
|
|
9356
|
+
? { registrarOrgIds: registrarOrgIdsOf(org, scope.recordedIds()) }
|
|
9357
|
+
: {}),
|
|
9235
9358
|
});
|
|
9236
|
-
if (refChanged) {
|
|
9359
|
+
if (refChanged || headMoved) {
|
|
9237
9360
|
const now = scope.recordedIds();
|
|
9238
9361
|
const brokerDeployed = Boolean(now[PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY]);
|
|
9362
|
+
const names = new Map(ownAuthBuildApps(org, now).map((app) => [app.id, app.name]));
|
|
9239
9363
|
for (const id of gitApps) {
|
|
9240
9364
|
// The broker crash-loops until its inputs exist; it was never deployed, so it is not now.
|
|
9241
9365
|
if (id === now[PLATFORM_AUTH_BROKER_SCOPE_KEY] && !brokerDeployed)
|
|
9242
9366
|
continue;
|
|
9243
9367
|
await deps.coolify.deploy(id, true);
|
|
9368
|
+
// Recorded after the deploy was queued: a failed one leaves the old build, so the next plan repeats it.
|
|
9369
|
+
const name = names.get(id);
|
|
9370
|
+
if (head && name)
|
|
9371
|
+
scope.recordScopeId(builtCommitScopeKey(name), head);
|
|
9244
9372
|
}
|
|
9245
9373
|
}
|
|
9246
9374
|
scope.recordScopeId(OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, hostname);
|
|
@@ -27,6 +27,19 @@
|
|
|
27
27
|
* no live measurement to pin it to instead"). A caller whose app is known to
|
|
28
28
|
* be slower or faster to boot passes its own.
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* #1054: the two fields that let Coolify run a rolling update for an app other containers reach by a fixed name.
|
|
32
|
+
* Coolify starts the new container beside the old one, waits for its check, then stops the old one, only when the
|
|
33
|
+
* app has no fixed container name (`custom_internal_name`) and the consistent-name flag is off. A network alias
|
|
34
|
+
* answers on the Docker network under the same name throughout the swap, so callers send this instead of
|
|
35
|
+
* `custom_internal_name`. The app also needs a health check to wait on (`healthCheckSettings` or
|
|
36
|
+
* {@link IMAGE_HEALTHCHECK_SETTINGS}).
|
|
37
|
+
*/
|
|
38
|
+
export declare function rollingUpdateSettings(alias: string): {
|
|
39
|
+
custom_network_aliases: string;
|
|
40
|
+
is_consistent_container_name_enabled: false;
|
|
41
|
+
custom_internal_name: null;
|
|
42
|
+
};
|
|
30
43
|
/** The four Coolify `updateApplication` fields {@link healthCheckSettings} returns. */
|
|
31
44
|
export interface HealthCheckPatch {
|
|
32
45
|
health_check_enabled: true;
|
|
@@ -27,6 +27,23 @@
|
|
|
27
27
|
* no live measurement to pin it to instead"). A caller whose app is known to
|
|
28
28
|
* be slower or faster to boot passes its own.
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* #1054: the two fields that let Coolify run a rolling update for an app other containers reach by a fixed name.
|
|
32
|
+
* Coolify starts the new container beside the old one, waits for its check, then stops the old one, only when the
|
|
33
|
+
* app has no fixed container name (`custom_internal_name`) and the consistent-name flag is off. A network alias
|
|
34
|
+
* answers on the Docker network under the same name throughout the swap, so callers send this instead of
|
|
35
|
+
* `custom_internal_name`. The app also needs a health check to wait on (`healthCheckSettings` or
|
|
36
|
+
* {@link IMAGE_HEALTHCHECK_SETTINGS}).
|
|
37
|
+
*/
|
|
38
|
+
export function rollingUpdateSettings(alias) {
|
|
39
|
+
// `custom_internal_name: null` clears the fixed name an app made by an earlier foundry still has: with it set
|
|
40
|
+
// Coolify refuses the rolling update whatever else is patched.
|
|
41
|
+
return {
|
|
42
|
+
custom_network_aliases: alias,
|
|
43
|
+
is_consistent_container_name_enabled: false,
|
|
44
|
+
custom_internal_name: null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
30
47
|
/** Archon's own number (see this module's doc comment) -- the default every other caller inherits unless it knows better. */
|
|
31
48
|
export const DEFAULT_HEALTH_CHECK_START_PERIOD_SECONDS = 30;
|
|
32
49
|
/**
|