@wtfalch/foundry 0.71.0 → 0.72.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/native-build.d.ts +4 -1
- package/dist/native-build.js +8 -2
- 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
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
|
/**
|
package/dist/plan/own-diff.d.ts
CHANGED
|
@@ -52,7 +52,35 @@ export declare const OWN_AUTH_BLOCKED_KEY = "own-auth";
|
|
|
52
52
|
export { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY };
|
|
53
53
|
/** The auth git ref an own org's sign-in last deployed, so a changed declared ref plans the step again. */
|
|
54
54
|
export declare const OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY = "own_auth_applied_git_ref";
|
|
55
|
+
/** The Git repository the company's login app, in-app login app and broker are built from. */
|
|
56
|
+
export declare const OWN_AUTH_REPOSITORY = "wtfalch/auth";
|
|
57
|
+
/** What the host read of the declared auth branch: its head commit, or why it could not be read (fixed text). */
|
|
58
|
+
export type OwnAuthHead = {
|
|
59
|
+
commit: string;
|
|
60
|
+
} | {
|
|
61
|
+
failed: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* True for a declared auth ref whose head can move: not a commit and not a release tag (`v1.2.3`, `1.2`).
|
|
65
|
+
* A tag or a commit already names one build, so it is never read.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isBranchRef(ref: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The company's auth apps a moved branch head redeploys, with the name their `built_commit_<name>` is recorded
|
|
70
|
+
* under. The in-app login app is listed once it is recorded; the broker once it was ever deployed (before that it
|
|
71
|
+
* crash-loops on missing inputs and is not redeployed, so it never records a build).
|
|
72
|
+
*/
|
|
73
|
+
export declare function ownAuthBuildApps(org: Pick<DeclaredOrg, 'slug'>, recordedIds: Readonly<Record<string, string>>): Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
}>;
|
|
55
77
|
export interface PlanOwnAuthOptions {
|
|
78
|
+
/**
|
|
79
|
+
* #886: the head of the declared auth branch, read by the executor (`FoundryStepExecutor.ownAuthHead`).
|
|
80
|
+
* Absent: not read (a tag, a commit, or a host without the read), so only the declared ref decides.
|
|
81
|
+
* A failed read plans nothing from the head and is reported.
|
|
82
|
+
*/
|
|
83
|
+
authHead?: OwnAuthHead;
|
|
56
84
|
org: DeclaredOrg;
|
|
57
85
|
recordedIds: Readonly<Record<string, string>>;
|
|
58
86
|
readiness: OrgPlatformReadiness;
|
|
@@ -76,6 +104,10 @@ export interface PlanOwnAuthOptions {
|
|
|
76
104
|
signinDbBackupConfigured?: boolean;
|
|
77
105
|
/** The host's ZITADEL port can write the sign-in mail text and look. Default true. False: the marker is not required. */
|
|
78
106
|
signinMailWritable?: boolean;
|
|
107
|
+
/** The host's ZITADEL port can make the broker's registrar token. True: it is asked for. False: reported blocked. Absent: neither. */
|
|
108
|
+
registrarTokenWritable?: boolean;
|
|
109
|
+
/** The registrar token's held-key ref holds a value, so a rerun after a crash delivers it. Default false. */
|
|
110
|
+
registrarTokenHeld?: boolean;
|
|
79
111
|
}
|
|
80
112
|
/**
|
|
81
113
|
* An own org (`services.own`) runs its own sign-in: ZITADEL, the login app
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -3,18 +3,20 @@ import { sfuSlotPorts } from '../calls-sfu.js';
|
|
|
3
3
|
import { SFU_SLOT_MAX, ownsServices } from '../org.js';
|
|
4
4
|
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey } from './broker-entries.js';
|
|
6
|
+
import { REGISTRAR_TOKEN_SCOPE_KEY, REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY, registrarTokenDue, } from './broker-registrar-token.js';
|
|
7
|
+
import { builtCommitScopeKey } from './build-record.js';
|
|
6
8
|
import { ZITADEL_FILES_PROJECT_SCOPE_KEY } from './core-files.js';
|
|
7
9
|
import { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, ownAuthApplied } from './login-host.js';
|
|
8
10
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, } from './mail-relay.js';
|
|
9
11
|
import { mailStoreIsLegacy, mailStoreSwitchDescription } from './mail-store.js';
|
|
10
|
-
import { ownHostnames } from './own-hostnames.js';
|
|
12
|
+
import { ownHostnames, passkeyRpIdFor } from './own-hostnames.js';
|
|
11
13
|
import { JOBS_PUBLIC_URL_SCOPE_KEY, OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY } from './own-jobs-route.js';
|
|
12
14
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
|
|
13
15
|
import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
|
|
14
|
-
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_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
16
|
+
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
15
17
|
import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
|
|
16
|
-
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';
|
|
17
|
-
import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn } from './sign-in.js';
|
|
18
|
+
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
19
|
+
import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn, signInLoginNames } from './sign-in.js';
|
|
18
20
|
import { signinDbBackupMissing } from './signin-db-backup.js';
|
|
19
21
|
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, signinDbCertDue } from './signin-db-tls.js';
|
|
20
22
|
import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db.js';
|
|
@@ -126,6 +128,39 @@ export const OWN_AUTH_BLOCKED_KEY = 'own-auth';
|
|
|
126
128
|
export { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY };
|
|
127
129
|
/** The auth git ref an own org's sign-in last deployed, so a changed declared ref plans the step again. */
|
|
128
130
|
export const OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY = 'own_auth_applied_git_ref';
|
|
131
|
+
/** The Git repository the company's login app, in-app login app and broker are built from. */
|
|
132
|
+
export const OWN_AUTH_REPOSITORY = 'wtfalch/auth';
|
|
133
|
+
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
134
|
+
/**
|
|
135
|
+
* True for a declared auth ref whose head can move: not a commit and not a release tag (`v1.2.3`, `1.2`).
|
|
136
|
+
* A tag or a commit already names one build, so it is never read.
|
|
137
|
+
*/
|
|
138
|
+
export function isBranchRef(ref) {
|
|
139
|
+
return !FULL_SHA.test(ref) && !/^v?\d+\.\d+/.test(ref);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The company's auth apps a moved branch head redeploys, with the name their `built_commit_<name>` is recorded
|
|
143
|
+
* under. The in-app login app is listed once it is recorded; the broker once it was ever deployed (before that it
|
|
144
|
+
* crash-loops on missing inputs and is not redeployed, so it never records a build).
|
|
145
|
+
*/
|
|
146
|
+
export function ownAuthBuildApps(org, recordedIds) {
|
|
147
|
+
const target = orgTarget(org);
|
|
148
|
+
const apps = [
|
|
149
|
+
{ id: recordedIds[PLATFORM_AUTH_LOGIN_SCOPE_KEY], name: target.names.authLogin },
|
|
150
|
+
{ id: recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY], name: signInLoginNames(target).name },
|
|
151
|
+
{
|
|
152
|
+
id: recordedIds[PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY]
|
|
153
|
+
? recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY]
|
|
154
|
+
: undefined,
|
|
155
|
+
name: target.names.authBroker,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
return apps.filter((app) => Boolean(app.id));
|
|
159
|
+
}
|
|
160
|
+
/** True when the read head differs from the commit any of the company's auth apps was last built from. */
|
|
161
|
+
function authHeadMoved(org, recordedIds, head) {
|
|
162
|
+
return ownAuthBuildApps(org, recordedIds).some((app) => recordedIds[builtCommitScopeKey(app.name)] !== head);
|
|
163
|
+
}
|
|
129
164
|
/**
|
|
130
165
|
* An own org (`services.own`) runs its own sign-in: ZITADEL, the login app
|
|
131
166
|
* and the broker in the org's own Coolify project, at `auth.<domain>`.
|
|
@@ -197,7 +232,21 @@ export function planOwnAuth(options) {
|
|
|
197
232
|
return blocked('the host supplies no backup bucket for the sign-in database');
|
|
198
233
|
}
|
|
199
234
|
}
|
|
235
|
+
const registrarDue = options.registrarTokenWritable === true && registrarTokenDue(org, recordedIds);
|
|
236
|
+
// A token minted whose hold did not finish is never minted again: a person removes the marker.
|
|
237
|
+
if (registrarDue &&
|
|
238
|
+
recordedIds[REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY] &&
|
|
239
|
+
!recordedIds[REGISTRAR_TOKEN_SCOPE_KEY] &&
|
|
240
|
+
!options.registrarTokenHeld) {
|
|
241
|
+
return blocked(`the broker's registrar token of ZITADEL machine user ${recordedIds[REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY]} was minted but never confirmed held; remove the recorded key ${REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY} to make a new one`);
|
|
242
|
+
}
|
|
200
243
|
const hostname = names.auth;
|
|
244
|
+
// #886: a branch head that moved past what the auth apps were built from plans the step again. A failed read
|
|
245
|
+
// plans nothing from the head and is reported, so a broken read never loops.
|
|
246
|
+
const head = options.authHead;
|
|
247
|
+
const headCommit = head && 'commit' in head ? head.commit : undefined;
|
|
248
|
+
const headFailed = head && 'failed' in head ? head.failed : undefined;
|
|
249
|
+
const headMoved = headCommit !== undefined && authHeadMoved(org, recordedIds, headCommit);
|
|
201
250
|
// The broker starts only on a valid `AUTH_APPS`, and the login client is minted only once it does:
|
|
202
251
|
// the first broker entry is what lets this step finish, as the platform's first registration does.
|
|
203
252
|
// Archon's registration is an entry too; the platform records it, not this org, so it comes in
|
|
@@ -219,7 +268,7 @@ export function planOwnAuth(options) {
|
|
|
219
268
|
recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
|
|
220
269
|
// The login apps' mail env (`LOGIN_MAIL_FROM`, the Lettermint key); a stack from before it is written again.
|
|
221
270
|
recordedIds[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] ===
|
|
222
|
-
authLoginEnvMarker(`noreply@${names.mailDomain}
|
|
271
|
+
authLoginEnvMarker(`noreply@${names.mailDomain}`, passkeyRpIdFor(org)) &&
|
|
223
272
|
// The code mail carries the company's name and design; an older apply, or a renamed company, writes it again.
|
|
224
273
|
(options.signinMailWritable === false ||
|
|
225
274
|
recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] === signinMailMarker(org.displayName)) &&
|
|
@@ -234,15 +283,23 @@ export function planOwnAuth(options) {
|
|
|
234
283
|
!signinDbCertDue(recordedIds, options.now ?? Date.now()) &&
|
|
235
284
|
!signinDbBackupMissing(recordedIds) &&
|
|
236
285
|
// ZITADEL was started on the TLS database and never seen healthy there: the step starts it again.
|
|
237
|
-
!recordedIds[SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY]
|
|
286
|
+
!recordedIds[SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY] &&
|
|
287
|
+
!headMoved &&
|
|
288
|
+
// The broker's registrar token (a company with customers or apps): made, or adopted, and covering the recorded organisations.
|
|
289
|
+
!registrarDue) {
|
|
238
290
|
// Applied and otherwise done: say why the mail stays ZITADEL's stock text, once.
|
|
239
291
|
const mailMissing = options.signinMailWritable === false &&
|
|
240
292
|
recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] !== signinMailMarker(org.displayName);
|
|
293
|
+
const registrarMissing = options.registrarTokenWritable === false && registrarTokenDue(org, recordedIds);
|
|
241
294
|
return {
|
|
242
295
|
step: null,
|
|
243
|
-
blocked:
|
|
244
|
-
? `${org.slug}'s
|
|
245
|
-
:
|
|
296
|
+
blocked: headFailed
|
|
297
|
+
? `${org.slug}'s auth apps were not checked for a newer auth commit: ${headFailed}`
|
|
298
|
+
: registrarMissing
|
|
299
|
+
? `${org.slug}'s sign-in broker has no registrar token: the host cannot make it, so its apps cannot register sign-in`
|
|
300
|
+
: mailMissing
|
|
301
|
+
? `${org.slug}'s sign-in mail keeps ZITADEL's default text: the host cannot write message texts`
|
|
302
|
+
: null,
|
|
246
303
|
};
|
|
247
304
|
}
|
|
248
305
|
return {
|
|
@@ -252,7 +309,7 @@ export function planOwnAuth(options) {
|
|
|
252
309
|
system: 'coolify',
|
|
253
310
|
kind: 'own-auth',
|
|
254
311
|
key: org.id,
|
|
255
|
-
desired: { hostname, gitRef },
|
|
312
|
+
desired: { hostname, gitRef, ...(headCommit ? { head: headCommit } : {}) },
|
|
256
313
|
},
|
|
257
314
|
step: {
|
|
258
315
|
description: `provision ${org.slug}'s own sign-in at ${hostname}`,
|
|
@@ -27,3 +27,11 @@ export interface OwnHostnames {
|
|
|
27
27
|
* {@link loginHostnameFor}, so the two cannot disagree.
|
|
28
28
|
*/
|
|
29
29
|
export declare function ownHostnames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'issuer' | 'mail'>): OwnHostnames;
|
|
30
|
+
/**
|
|
31
|
+
* The WebAuthn RP ID for a company's login apps (`LOGIN_PASSKEY_RP_ID`): the company's own domain, which
|
|
32
|
+
* `auth.<domain>` and the in-app hosts (`agora.<domain>`, `boule.<domain>`) all sit under, so one passkey
|
|
33
|
+
* works on each. Undefined for a company without its own sign-in domain: its `<slug>-auth.oikos.host` shares
|
|
34
|
+
* `oikos.host` with every other company, so an RP ID there would span companies. The login app refuses a
|
|
35
|
+
* host outside the RP ID, so a company's hosts stay under its domain or the variable stays unset.
|
|
36
|
+
*/
|
|
37
|
+
export declare function passkeyRpIdFor(org: Pick<DeclaredOrg, 'domain' | 'issuer'>): string | undefined;
|
|
@@ -23,3 +23,17 @@ export function ownHostnames(org) {
|
|
|
23
23
|
mailDomain: mailOnDomain && domain ? domain : `${slug}.${NO_DOMAIN_ZONE}`,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
const BARE_DOMAIN = /^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
27
|
+
/**
|
|
28
|
+
* The WebAuthn RP ID for a company's login apps (`LOGIN_PASSKEY_RP_ID`): the company's own domain, which
|
|
29
|
+
* `auth.<domain>` and the in-app hosts (`agora.<domain>`, `boule.<domain>`) all sit under, so one passkey
|
|
30
|
+
* works on each. Undefined for a company without its own sign-in domain: its `<slug>-auth.oikos.host` shares
|
|
31
|
+
* `oikos.host` with every other company, so an RP ID there would span companies. The login app refuses a
|
|
32
|
+
* host outside the RP ID, so a company's hosts stay under its domain or the variable stays unset.
|
|
33
|
+
*/
|
|
34
|
+
export function passkeyRpIdFor(org) {
|
|
35
|
+
const domain = org.domain?.toLowerCase();
|
|
36
|
+
if (!domain || !org.issuer.usesDomain || !BARE_DOMAIN.test(domain))
|
|
37
|
+
return undefined;
|
|
38
|
+
return domain;
|
|
39
|
+
}
|
|
@@ -61,6 +61,8 @@ export interface ServiceTarget {
|
|
|
61
61
|
};
|
|
62
62
|
readonly refs: {
|
|
63
63
|
instanceBearer: string;
|
|
64
|
+
/** The sign-in broker's ZITADEL registrar token (`ZITADEL_REGISTRAR_TOKEN`), held after its one mint. */
|
|
65
|
+
registrarToken: string;
|
|
64
66
|
authAdmin: string;
|
|
65
67
|
brokerMailKey: string;
|
|
66
68
|
/** The connection string of the database for the broker's customer workspaces: foundry holds it after making the database, or the operator holds one first. */
|
|
@@ -209,4 +211,9 @@ export interface OwnServicesPort {
|
|
|
209
211
|
/** Builds the ZITADEL port for one company instance. */
|
|
210
212
|
zitadelFor(instance: OwnAuthInstance): ZitadelPort;
|
|
211
213
|
provision: OwnServicesProvision;
|
|
214
|
+
/**
|
|
215
|
+
* #886: the head commit (40 hex characters) of `ref` in the Git repository `repository` (`owner/name`).
|
|
216
|
+
* Absent: a moved auth branch is not noticed. The executor puts a timeout on every call.
|
|
217
|
+
*/
|
|
218
|
+
readBranchHead?(repository: string, ref: string): Promise<string>;
|
|
212
219
|
}
|
|
@@ -46,6 +46,7 @@ export function platformTarget() {
|
|
|
46
46
|
},
|
|
47
47
|
refs: {
|
|
48
48
|
instanceBearer: 'zitadel:platform:instanceBearer',
|
|
49
|
+
registrarToken: 'zitadel:platform:registrarToken',
|
|
49
50
|
authAdmin: 'zitadel:platform:admin',
|
|
50
51
|
brokerMailKey: 'lettermint:platform:apiToken',
|
|
51
52
|
brokerWorkspacesDatabase: 'auth:platform:workspacesDatabaseUrl',
|
|
@@ -103,6 +104,7 @@ export function orgTarget(org) {
|
|
|
103
104
|
},
|
|
104
105
|
refs: {
|
|
105
106
|
instanceBearer: `zitadel:org:${s}:instanceBearer`,
|
|
107
|
+
registrarToken: `zitadel:org:${s}:registrarToken`,
|
|
106
108
|
authAdmin: `zitadel:org:${s}:admin`,
|
|
107
109
|
brokerMailKey: `lettermint:org:${s}:apiToken`,
|
|
108
110
|
brokerWorkspacesDatabase: `auth:org:${s}:workspacesDatabaseUrl`,
|
|
@@ -18,6 +18,7 @@ import type { GithubDeployKeysPort } from './github-deploy-keys.js';
|
|
|
18
18
|
import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
|
|
19
19
|
import { type LoginHostDiscoveryPort } from './login-host.js';
|
|
20
20
|
import { type OrgPostgresMigrationConfirmation } from './org-postgres-migrate.js';
|
|
21
|
+
import type { OwnAuthHead } from './own-diff.js';
|
|
21
22
|
import { type EdgePortName, type HeldKeyResolver, type PlatformOwnerInput } from './platform-org.js';
|
|
22
23
|
import type { CallsSfuConnectionPort } from './platform-sfu.js';
|
|
23
24
|
import { type ReleaseContext, type ReleaseHost } from './release-host.js';
|
|
@@ -212,8 +213,12 @@ export interface StepExecutor {
|
|
|
212
213
|
}>;
|
|
213
214
|
/** Whether the executor can mint an own org's Lettermint key: a Lettermint port and a reader for the held ref. */
|
|
214
215
|
ownMailKeyMintable?(): boolean;
|
|
216
|
+
/** Whether the ZITADEL port can set the customers organisation's login policy. Absent: planned, as before. */
|
|
217
|
+
customersLoginPolicyAvailable?(): boolean;
|
|
215
218
|
/** C3: whether the ZITADEL port can mint and revoke a customers directory credential. */
|
|
216
219
|
customersDirectoryAvailable?(): boolean;
|
|
220
|
+
/** Whether the ZITADEL port can grant the directory user its customer removal role. */
|
|
221
|
+
customersDirectoryRolesAvailable?(): boolean;
|
|
217
222
|
/** C3: whether the host supplied `mintBouleArchonCredential`. */
|
|
218
223
|
bouleArchonCredentialAvailable?(): boolean;
|
|
219
224
|
/** Every Coolify server with whether it is usable and reachable; blue/green plans copies on each. Optional: an executor without it plans no blue/green. */
|
|
@@ -264,6 +269,12 @@ export interface StepExecutor {
|
|
|
264
269
|
ownSigninDbBackupConfigured?(): boolean;
|
|
265
270
|
/** Whether the host's company ZITADEL port can write the sign-in mail text and look. Absent: true. */
|
|
266
271
|
ownSigninMailWritable?(): boolean;
|
|
272
|
+
/** #886: reads the head of the declared auth branch for an own org; `undefined` when there is nothing to read. */
|
|
273
|
+
ownAuthHead?(gitRef: string): Promise<OwnAuthHead | undefined>;
|
|
274
|
+
/** Whether the host's ZITADEL port can grant the shared sign-in's registrar user a company's organisations. Absent: false. */
|
|
275
|
+
registrarGrantsAvailable?(): boolean;
|
|
276
|
+
/** Whether the host's company ZITADEL port can make the broker's registrar token. Absent: the planner neither asks for it nor reports it. */
|
|
277
|
+
ownRegistrarTokenWritable?(): boolean;
|
|
267
278
|
/**
|
|
268
279
|
* Whether the host's scoped ports for this own org include a files-organisation
|
|
269
280
|
* port. The planner plans no `files:organisation` step for an own org while
|
|
@@ -1022,8 +1033,12 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
1022
1033
|
* only the ref.
|
|
1023
1034
|
*/
|
|
1024
1035
|
private heldEnvReadiness;
|
|
1036
|
+
/** #886: the head of an own org's declared auth branch, or nothing when the org is not own or the executor has no read. */
|
|
1037
|
+
private ownAuthHeadFor;
|
|
1025
1038
|
/** An own org's Lettermint key is held. A resolver that throws counts as not held. */
|
|
1026
1039
|
private ownMailKeyHeld;
|
|
1040
|
+
/** An own org's registrar token is held (a rerun after a crash delivers it instead of blocking). */
|
|
1041
|
+
private ownRegistrarTokenHeld;
|
|
1027
1042
|
/** Which of an own org's undelivered-secret refs hold a value now, so a rerun resumes instead of blocking. */
|
|
1028
1043
|
private ownResumableRefs;
|
|
1029
1044
|
/**
|