@wtfalch/foundry 0.60.0 → 0.61.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/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +5 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/org.d.ts +163 -22
- package/dist/org.js +419 -47
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +41 -1
- package/dist/plan/blue-green.js +80 -2
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/desktop-callback.js +12 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +373 -42
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +191 -1
- package/dist/plan/executor.js +1505 -90
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +2 -2
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.js +12 -1
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +7 -2
- package/dist/plan/post-logout-uris.js +7 -1
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +11 -0
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +10 -6
- package/dist/store/postgres.js +12 -2
- package/dist/store/schema.d.ts +37 -1
- package/dist/store/schema.js +4 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +18 -1
|
@@ -7,10 +7,11 @@ import { waitForNewDeployment } from './deploy-health.js';
|
|
|
7
7
|
import { commandHealthCheckSettings, wgetProbe } from './health-check-settings.js';
|
|
8
8
|
import { ENTRY_CONTAINER_LABELS } from './proxy-labels.js';
|
|
9
9
|
import { remoteDatabaseAddress, remoteDatabaseHost } from './remote-database.js';
|
|
10
|
-
import { buildStageEnv } from './stage-env.js';
|
|
10
|
+
import { STAGE_SIGN_IN_NAMES, buildStageEnv } from './stage-env.js';
|
|
11
11
|
import { buildStageProbes, runStageProbes, stageProbesRecord, } from './stage-guards.js';
|
|
12
12
|
import { assertStageIsolated, envClass, stageIsolationHash, } from './stage-isolation.js';
|
|
13
13
|
import { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_APP_SCOPE_KEY, STAGE_SINK_HTTP_PORT, STAGE_SINK_IMAGE_TAG, STAGE_SINK_REPOSITORY, stageMailEnv, stageSinkAppName, } from './stage-sink.js';
|
|
14
|
+
import { readWorkspaceCommit, wsKey } from './workspace.js';
|
|
14
15
|
/** How long one Coolify call of the sink may take. */
|
|
15
16
|
const SINK_CALL_TIMEOUT_MS = 30_000;
|
|
16
17
|
/** Env names whose value is a credential: registered before they are copied. */
|
|
@@ -97,6 +98,30 @@ export async function applyBlueGreenServer(deps, subject, server) {
|
|
|
97
98
|
deps.record(bgKey.applied(id, server.uuid), structureHash(subject, { primary: isPrimary }));
|
|
98
99
|
await deps.persist();
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Applies a server workspace on the primary: the stage mail sink, then the stage app with its own empty
|
|
103
|
+
* database, the stage guards and the gate, exactly as a blue/green stage, and no colors. The commit the stage
|
|
104
|
+
* deployed is recorded as `ws.<id>.commit` for the live app to pin to. A failure halfway leaves the sink, app
|
|
105
|
+
* and database already made, recorded by id; the next run finds them and does the rest. Stage fails closed for
|
|
106
|
+
* itself: a refused guard records `bg.<id>.stagestate` and writes nothing to the stage app.
|
|
107
|
+
*/
|
|
108
|
+
export async function applyWorkspaceServer(deps, subject, serverUuid) {
|
|
109
|
+
const sinkReady = await ensureStageSink(deps, subject, serverUuid);
|
|
110
|
+
await ensureStage(deps, subject, serverUuid);
|
|
111
|
+
if (!sinkReady) {
|
|
112
|
+
deps.record(bgKey.stageState(subject.id), 'blocked:sink');
|
|
113
|
+
await deps.persist();
|
|
114
|
+
}
|
|
115
|
+
const stageId = deps.recorded()[bgKey.app(subject.id, serverUuid, 'stage')];
|
|
116
|
+
if (stageId && deps.recorded()[bgKey.stageState(subject.id)] === 'ok') {
|
|
117
|
+
const commit = (await readWorkspaceCommit(deps.coolify, stageId)) ??
|
|
118
|
+
deps.recorded()[bgKey.stage(subject.id)];
|
|
119
|
+
if (commit) {
|
|
120
|
+
deps.record(wsKey.commit(subject.id), commit);
|
|
121
|
+
await deps.persist();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
100
125
|
/** Writes the balancer's config with the recorded weights and records them for this server. */
|
|
101
126
|
export async function applyBlueGreenWeights(deps, subject, server) {
|
|
102
127
|
const weights = parseWeights(deps.recorded()[bgKey.weights(subject.id)]);
|
|
@@ -109,15 +134,14 @@ export async function applyBlueGreenWeights(deps, subject, server) {
|
|
|
109
134
|
}
|
|
110
135
|
async function writeLbConfig(deps, subject, server, lbId, weights) {
|
|
111
136
|
const onPrimary = server.isPrimary;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
deps.record(bgKey.pinKey(subject.id), pinKey);
|
|
116
|
-
await deps.persist();
|
|
117
|
-
}
|
|
137
|
+
const pinKey = await deps.pinKey();
|
|
138
|
+
// The idle slot is the one `bg.<id>.live` does not name; the preview router follows it on every weight write.
|
|
139
|
+
const idle = deps.recorded()[bgKey.live(subject.id)] === 'green' ? 'blue' : 'green';
|
|
118
140
|
const content = renderLbConfig({
|
|
119
141
|
weights,
|
|
120
142
|
pinKey,
|
|
143
|
+
idle,
|
|
144
|
+
reporting: subject.reporting,
|
|
121
145
|
backends: {
|
|
122
146
|
blue: [colorBackend(subject, 'blue', onPrimary)],
|
|
123
147
|
green: [colorBackend(subject, 'green', onPrimary)],
|
|
@@ -211,14 +235,16 @@ async function ensureGitApp(deps, subject, input) {
|
|
|
211
235
|
await deps.persist();
|
|
212
236
|
return found.uuid;
|
|
213
237
|
}
|
|
214
|
-
const
|
|
238
|
+
const githubAppUuid = deps.githubAppUuid
|
|
239
|
+
? await deps.githubAppUuid(subject.repo)
|
|
240
|
+
: (await coolify.requireGithubApp()).uuid;
|
|
215
241
|
const created = await coolify.createGithubAppApplication({
|
|
216
242
|
is_preview_deployments_enabled: false,
|
|
217
243
|
project_uuid: deps.projectUuid,
|
|
218
244
|
server_uuid: input.server,
|
|
219
245
|
environment_name: deps.environment.name,
|
|
220
246
|
environment_uuid: deps.environment.uuid,
|
|
221
|
-
github_app_uuid:
|
|
247
|
+
github_app_uuid: githubAppUuid,
|
|
222
248
|
git_repository: subject.repo,
|
|
223
249
|
git_branch: input.branch,
|
|
224
250
|
build_pack: subject.buildPack,
|
|
@@ -319,7 +345,7 @@ async function ensureLoadBalancer(deps, subject, serverUuid, name, isPrimary) {
|
|
|
319
345
|
* the sink deploys; a failure clears it, logs fixed text and the error name, and returns false. Production's
|
|
320
346
|
* structure goes on (stage then holds no mail env and the copy stays refused).
|
|
321
347
|
*/
|
|
322
|
-
async function ensureStageSink(deps, subject, serverUuid) {
|
|
348
|
+
export async function ensureStageSink(deps, subject, serverUuid) {
|
|
323
349
|
const org = deps.stageSink;
|
|
324
350
|
if (!org)
|
|
325
351
|
return true;
|
|
@@ -377,7 +403,7 @@ async function ensureStageSink(deps, subject, serverUuid) {
|
|
|
377
403
|
return true;
|
|
378
404
|
}
|
|
379
405
|
/** The stage app and its own empty database, on the primary. Its env comes from the allowlist, never from a copy of the live app's. */
|
|
380
|
-
async function ensureStage(deps, subject, serverUuid) {
|
|
406
|
+
export async function ensureStage(deps, subject, serverUuid) {
|
|
381
407
|
const { coolify } = deps;
|
|
382
408
|
const name = stageAppName(subject.base);
|
|
383
409
|
const appId = await stageAppId(deps, subject, name, serverUuid);
|
|
@@ -419,12 +445,12 @@ async function ensureStage(deps, subject, serverUuid) {
|
|
|
419
445
|
}
|
|
420
446
|
}
|
|
421
447
|
const kind = stageKindOf(subject);
|
|
422
|
-
const prodEnv = await coolify.listEnvs(deps.primaryBlueAppId);
|
|
448
|
+
const prodEnv = deps.primaryBlueAppId ? await coolify.listEnvs(deps.primaryBlueAppId) : [];
|
|
423
449
|
const greenId = deps.recorded()[bgKey.app(subject.id, serverUuid, 'green')];
|
|
424
450
|
const greenEnv = greenId ? await coolify.listEnvs(greenId) : [];
|
|
425
451
|
const { env } = buildStageEnv({
|
|
426
452
|
kind,
|
|
427
|
-
appUrl: stageAppUrl(subject),
|
|
453
|
+
appUrl: deps.stageAppUrl ?? stageAppUrl(subject),
|
|
428
454
|
logDir: deps.logDir,
|
|
429
455
|
database,
|
|
430
456
|
prod: prodEnv,
|
|
@@ -434,8 +460,14 @@ async function ensureStage(deps, subject, serverUuid) {
|
|
|
434
460
|
deps.secret(value);
|
|
435
461
|
return value;
|
|
436
462
|
},
|
|
463
|
+
...(deps.signIn ? { signIn: deps.signIn } : {}),
|
|
437
464
|
});
|
|
438
465
|
const stageEnv = [...env];
|
|
466
|
+
const signInEnv = deps.signIn ? env.filter((e) => STAGE_SIGN_IN_NAMES.includes(e.key)) : [];
|
|
467
|
+
// A secret kept from an earlier apply was not minted by this run, so it is registered here.
|
|
468
|
+
for (const e of signInEnv)
|
|
469
|
+
if (envClass(e.key) === 'secret')
|
|
470
|
+
deps.secret(e.value);
|
|
439
471
|
if (subject.reporting && deps.actionsKey) {
|
|
440
472
|
// Stage never shares a production secret: its key is its own, minted once and kept.
|
|
441
473
|
const have = (await coolify.listEnvs(appId)).find((e) => e.key === ACTIONS_KEY_ENV && !e.is_preview);
|
|
@@ -447,7 +479,7 @@ async function ensureStage(deps, subject, serverUuid) {
|
|
|
447
479
|
const sinkApplied = deps.stageSink && deps.recorded()[STAGE_SINK_APPLIED_SCOPE_KEY];
|
|
448
480
|
const mail = deps.stageSink && sinkApplied ? stageMailEnv(deps.stageSink) : [];
|
|
449
481
|
const stageHas = new Map((await coolify.listEnvs(appId)).filter((e) => !e.is_preview).map((e) => [e.key, e.value]));
|
|
450
|
-
const mailChanged = mail.some((e) => stageHas.get(e.key) !== e.value);
|
|
482
|
+
const mailChanged = [...mail, ...signInEnv].some((e) => stageHas.get(e.key) !== e.value);
|
|
451
483
|
stageEnv.push(...mail);
|
|
452
484
|
const stageSurface = {
|
|
453
485
|
env: stageEnv.map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
|
|
@@ -464,6 +496,9 @@ async function ensureStage(deps, subject, serverUuid) {
|
|
|
464
496
|
},
|
|
465
497
|
stage: stageSurface,
|
|
466
498
|
sinkMail: Object.fromEntries(mail.map((e) => [e.key, e.value])),
|
|
499
|
+
...(deps.signIn
|
|
500
|
+
? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
|
|
501
|
+
: {}),
|
|
467
502
|
});
|
|
468
503
|
if (violations.length > 0) {
|
|
469
504
|
// Stage fails closed for itself: nothing is written to it, production's structure goes on.
|
|
@@ -483,7 +518,12 @@ async function ensureStage(deps, subject, serverUuid) {
|
|
|
483
518
|
probeResult = await runStageProbes(buildStageProbes({
|
|
484
519
|
port: deps.stageGuards?.probes,
|
|
485
520
|
stage: stageValues,
|
|
486
|
-
database
|
|
521
|
+
// With no live app there is no production database to try the stage role against.
|
|
522
|
+
database: subject.database && deps.primaryBlueAppId !== null,
|
|
523
|
+
...(deps.signIn?.live.customersOrgId
|
|
524
|
+
? { liveCustomersOrgId: deps.signIn.live.customersOrgId }
|
|
525
|
+
: {}),
|
|
526
|
+
...(deps.signIn ? { signInRedirectUri: deps.signIn.redirectUri } : {}),
|
|
487
527
|
prod: new Map(prodEnv
|
|
488
528
|
.filter((e) => !e.is_preview && e.value !== undefined)
|
|
489
529
|
.map((e) => [e.key, e.value])),
|
|
@@ -46,7 +46,10 @@ export declare const bgKey: {
|
|
|
46
46
|
stageState: (id: string) => string;
|
|
47
47
|
/** The Coolify name of the blue app on the primary: the adopted single app's name, or `<base>-blue`. */
|
|
48
48
|
blueName: (id: string) => string;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The held ref of the balancer's pin key (a credential: it lets a caller pick a color). A value that is 64 hex
|
|
51
|
+
* characters is the older recorded key, moved to the held ref on the next read and then overwritten by the ref.
|
|
52
|
+
*/
|
|
50
53
|
pinKey: (id: string) => string;
|
|
51
54
|
/** The hash of the stage env name set and resource set that passed the isolation check. */
|
|
52
55
|
stageIsolation: (id: string) => string;
|
|
@@ -91,6 +94,34 @@ export declare function mintActionsKey(input: {
|
|
|
91
94
|
/** Registers the value for redaction before it is written anywhere. */
|
|
92
95
|
secret: (value: string) => void;
|
|
93
96
|
}): Promise<string>;
|
|
97
|
+
/** The held-key name of a subject's balancer pin key: one mint per subject. */
|
|
98
|
+
export declare const pinKeyName: (id: string) => string;
|
|
99
|
+
/** The held store the pin key lives in; `put` is absent on a host that cannot mint. */
|
|
100
|
+
export interface PinHeldKeys {
|
|
101
|
+
resolve(ref: string): Promise<string | null | undefined>;
|
|
102
|
+
put?(ref: string, value: string): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
interface PinKeyInput {
|
|
105
|
+
id: string;
|
|
106
|
+
ref: string;
|
|
107
|
+
recorded: () => Readonly<Record<string, string>>;
|
|
108
|
+
record: (key: string, value: string) => void;
|
|
109
|
+
persist: () => Promise<void>;
|
|
110
|
+
heldKeys: PinHeldKeys;
|
|
111
|
+
/** Registers the value for redaction before it is written anywhere. */
|
|
112
|
+
secret: (value: string) => void;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The balancer's pin key, held (CLAUDE.md, Platform steps, rules 1 and 6). Order: the held key; else the older
|
|
116
|
+
* recorded key (64 hex), put in the held store and replaced by the ref; else one mint, put first, then the ref is
|
|
117
|
+
* recorded and persisted. A crash between the put and the record reuses the held key. A recorded ref whose key is
|
|
118
|
+
* no longer held stops: it never mints again. Errors name the ref only.
|
|
119
|
+
*/
|
|
120
|
+
export declare function mintPinKey(input: PinKeyInput & {
|
|
121
|
+
generate: () => string;
|
|
122
|
+
}): Promise<string>;
|
|
123
|
+
/** Reads the pin key without minting: the held key, else the older recorded key, else null. */
|
|
124
|
+
export declare function readPinKey(input: Omit<PinKeyInput, 'record' | 'persist'>): Promise<string | null>;
|
|
94
125
|
/** The held-key name of an entry's health token (`sharedSecretRef(org.id, name)`): one mint per entry. */
|
|
95
126
|
export declare const healthTokenName: (id: string) => string;
|
|
96
127
|
/** What a color's container gets for the reporting health read. The release is left out until a digest is known. */
|
|
@@ -200,6 +231,10 @@ export declare const LB_COOKIE_NAME = "bg_color";
|
|
|
200
231
|
export declare const LB_COLOR_HEADER = "X-Foundry-Color";
|
|
201
232
|
/** A pinned request also carries this header with the entry's pin key, so a stranger cannot pin a color. */
|
|
202
233
|
export declare const LB_PIN_HEADER = "X-Foundry-Pin";
|
|
234
|
+
/** The cookie that sends a signed-in person with `releases:preview` to the idle slot. */
|
|
235
|
+
export declare const LB_PREVIEW_COOKIE = "bg_preview";
|
|
236
|
+
/** The path the balancer asks the live slot whether a request may see the idle slot. */
|
|
237
|
+
export declare const LB_PREVIEW_CHECK_PATH = "/api/foundry/preview-check";
|
|
203
238
|
export declare const LB_CONFIG_PATH = "/etc/traefik/dynamic/lb.yml";
|
|
204
239
|
/**
|
|
205
240
|
* The balancer's inline Dockerfile: a pinned Traefik with a file provider watching one directory.
|
|
@@ -218,6 +253,10 @@ export declare function renderLbConfig(input: {
|
|
|
218
253
|
weights: Weights;
|
|
219
254
|
backends: Record<Color, string[]>;
|
|
220
255
|
pinKey: string;
|
|
256
|
+
/** The slot that is not live: the `preview` router sends a cookie holder here. */
|
|
257
|
+
idle: Color;
|
|
258
|
+
/** True when the subject has a server that can answer the permission check; a static subject gets no router. */
|
|
259
|
+
reporting: boolean;
|
|
221
260
|
}): string;
|
|
222
261
|
/**
|
|
223
262
|
* The URL of one color's container on its own server's network. On the primary, blue is the app that was
|
|
@@ -324,3 +363,4 @@ export declare function planArchonBlueGreen(input: {
|
|
|
324
363
|
step: BlueGreenPlanStep | null;
|
|
325
364
|
unmanaged: UnmanagedLine[];
|
|
326
365
|
};
|
|
366
|
+
export {};
|
package/dist/plan/blue-green.js
CHANGED
|
@@ -55,7 +55,10 @@ export const bgKey = {
|
|
|
55
55
|
stageState: (id) => `bg.${id}.stagestate`,
|
|
56
56
|
/** The Coolify name of the blue app on the primary: the adopted single app's name, or `<base>-blue`. */
|
|
57
57
|
blueName: (id) => `bg.${id}.bluename`,
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* The held ref of the balancer's pin key (a credential: it lets a caller pick a color). A value that is 64 hex
|
|
60
|
+
* characters is the older recorded key, moved to the held ref on the next read and then overwritten by the ref.
|
|
61
|
+
*/
|
|
59
62
|
pinKey: (id) => `bg.${id}.pinkey`,
|
|
60
63
|
/** The hash of the stage env name set and resource set that passed the isolation check. */
|
|
61
64
|
stageIsolation: (id) => `bg.${id}.stageisolation`,
|
|
@@ -102,6 +105,56 @@ export async function mintActionsKey(input) {
|
|
|
102
105
|
await input.persist();
|
|
103
106
|
return key;
|
|
104
107
|
}
|
|
108
|
+
/** The held-key name of a subject's balancer pin key: one mint per subject. */
|
|
109
|
+
export const pinKeyName = (id) => `bg-pin-${id}`;
|
|
110
|
+
/**
|
|
111
|
+
* The balancer's pin key, held (CLAUDE.md, Platform steps, rules 1 and 6). Order: the held key; else the older
|
|
112
|
+
* recorded key (64 hex), put in the held store and replaced by the ref; else one mint, put first, then the ref is
|
|
113
|
+
* recorded and persisted. A crash between the put and the record reuses the held key. A recorded ref whose key is
|
|
114
|
+
* no longer held stops: it never mints again. Errors name the ref only.
|
|
115
|
+
*/
|
|
116
|
+
export async function mintPinKey(input) {
|
|
117
|
+
const held = await input.heldKeys.resolve(input.ref);
|
|
118
|
+
const recordedValue = input.recorded()[bgKey.pinKey(input.id)];
|
|
119
|
+
if (held) {
|
|
120
|
+
input.secret(held);
|
|
121
|
+
if (recordedValue !== input.ref) {
|
|
122
|
+
input.record(bgKey.pinKey(input.id), input.ref);
|
|
123
|
+
await input.persist();
|
|
124
|
+
}
|
|
125
|
+
return held;
|
|
126
|
+
}
|
|
127
|
+
const put = input.heldKeys.put?.bind(input.heldKeys);
|
|
128
|
+
if (!put)
|
|
129
|
+
throw new Error(`pin key ${input.ref} cannot be held: the host has no held store to write`);
|
|
130
|
+
if (recordedValue && PIN_KEY.test(recordedValue)) {
|
|
131
|
+
input.secret(recordedValue);
|
|
132
|
+
await put(input.ref, recordedValue);
|
|
133
|
+
input.record(bgKey.pinKey(input.id), input.ref);
|
|
134
|
+
await input.persist();
|
|
135
|
+
return recordedValue;
|
|
136
|
+
}
|
|
137
|
+
if (recordedValue) {
|
|
138
|
+
throw new Error(`pin key ${input.ref} was minted but is no longer held; restore the held value`);
|
|
139
|
+
}
|
|
140
|
+
const key = input.generate();
|
|
141
|
+
input.secret(key);
|
|
142
|
+
await put(input.ref, key);
|
|
143
|
+
input.record(bgKey.pinKey(input.id), input.ref);
|
|
144
|
+
await input.persist();
|
|
145
|
+
return key;
|
|
146
|
+
}
|
|
147
|
+
/** Reads the pin key without minting: the held key, else the older recorded key, else null. */
|
|
148
|
+
export async function readPinKey(input) {
|
|
149
|
+
const held = await input.heldKeys.resolve(input.ref);
|
|
150
|
+
const value = held ||
|
|
151
|
+
(PIN_KEY.test(input.recorded()[bgKey.pinKey(input.id)] ?? '')
|
|
152
|
+
? input.recorded()[bgKey.pinKey(input.id)]
|
|
153
|
+
: null);
|
|
154
|
+
if (value)
|
|
155
|
+
input.secret(value);
|
|
156
|
+
return value ?? null;
|
|
157
|
+
}
|
|
105
158
|
/** The held-key name of an entry's health token (`sharedSecretRef(org.id, name)`): one mint per entry. */
|
|
106
159
|
export const healthTokenName = (id) => `reporting-health-${id}`;
|
|
107
160
|
/** What a color's container gets for the reporting health read. The release is left out until a digest is known. */
|
|
@@ -246,6 +299,11 @@ export const LB_COLOR_HEADER = 'X-Foundry-Color';
|
|
|
246
299
|
/** A pinned request also carries this header with the entry's pin key, so a stranger cannot pin a color. */
|
|
247
300
|
export const LB_PIN_HEADER = 'X-Foundry-Pin';
|
|
248
301
|
const PIN_KEY = /^[0-9a-f]{64}$/;
|
|
302
|
+
/** The cookie that sends a signed-in person with `releases:preview` to the idle slot. */
|
|
303
|
+
export const LB_PREVIEW_COOKIE = 'bg_preview';
|
|
304
|
+
/** The path the balancer asks the live slot whether a request may see the idle slot. */
|
|
305
|
+
export const LB_PREVIEW_CHECK_PATH = '/api/foundry/preview-check';
|
|
306
|
+
const LB_PREVIEW_PRIORITY = 9000;
|
|
249
307
|
/** Above the default priority of `app` (its rule length), so a pinned request never reaches the weighted service. */
|
|
250
308
|
const LB_PIN_PRIORITY = 10000;
|
|
251
309
|
export const LB_CONFIG_PATH = '/etc/traefik/dynamic/lb.yml';
|
|
@@ -272,7 +330,8 @@ export const LB_IMAGE_REF = `traefik:${VERSIONS.traefik}`;
|
|
|
272
330
|
* line, so a reader sees both numbers. `backends` are the URLs of each color's containers, one per server.
|
|
273
331
|
*/
|
|
274
332
|
export function renderLbConfig(input) {
|
|
275
|
-
const { weights, backends, pinKey } = input;
|
|
333
|
+
const { weights, backends, pinKey, idle } = input;
|
|
334
|
+
const live = COLORS.find((color) => color !== idle);
|
|
276
335
|
if (!PIN_KEY.test(pinKey))
|
|
277
336
|
throw new Error('the load balancer pin key is not 64 hex characters');
|
|
278
337
|
const listed = COLORS.filter((color) => weights[color] > 0);
|
|
@@ -287,10 +346,29 @@ export function renderLbConfig(input) {
|
|
|
287
346
|
` entryPoints: [${LB_ENTRYPOINT}]`,
|
|
288
347
|
` service: ${color}`,
|
|
289
348
|
]),
|
|
349
|
+
...(input.reporting
|
|
350
|
+
? [
|
|
351
|
+
' preview:',
|
|
352
|
+
` rule: HeaderRegexp(\`Cookie\`, \`(^|;\\s*)${LB_PREVIEW_COOKIE}=1\`)`,
|
|
353
|
+
` priority: ${LB_PREVIEW_PRIORITY}`,
|
|
354
|
+
` entryPoints: [${LB_ENTRYPOINT}]`,
|
|
355
|
+
` service: ${idle}`,
|
|
356
|
+
' middlewares: [preview-check]',
|
|
357
|
+
]
|
|
358
|
+
: []),
|
|
290
359
|
' app:',
|
|
291
360
|
' rule: PathPrefix(`/`)',
|
|
292
361
|
` entryPoints: [${LB_ENTRYPOINT}]`,
|
|
293
362
|
' service: weighted',
|
|
363
|
+
...(input.reporting
|
|
364
|
+
? [
|
|
365
|
+
' middlewares:',
|
|
366
|
+
' preview-check:',
|
|
367
|
+
' forwardAuth:',
|
|
368
|
+
` address: ${backends[live][0]}${LB_PREVIEW_CHECK_PATH}`,
|
|
369
|
+
' trustForwardHeader: false',
|
|
370
|
+
]
|
|
371
|
+
: []),
|
|
294
372
|
' services:',
|
|
295
373
|
' weighted:',
|
|
296
374
|
' weighted:',
|
package/dist/plan/boule.d.ts
CHANGED
|
@@ -91,6 +91,12 @@ export declare const BOULE_ARCHON_CREDENTIAL_ENV = "ARCHON_CREDENTIAL";
|
|
|
91
91
|
/** The company's customers organisation in the sign-in service, and the read-only credential that lists its people (C3). */
|
|
92
92
|
export declare const BOULE_CUSTOMERS_ORG_ENV = "CUSTOMERS_ORGANISATION_ID";
|
|
93
93
|
export declare const BOULE_CUSTOMERS_CREDENTIAL_ENV = "CUSTOMERS_DIRECTORY_CREDENTIAL";
|
|
94
|
+
/**
|
|
95
|
+
* F5 (Q44): the company's test customers organisation, beside {@link BOULE_CUSTOMERS_ORG_ENV}, so a workspace copy
|
|
96
|
+
* of an app registers its sign-in against it. Written once the organisation is recorded (a server workspace with
|
|
97
|
+
* customers makes it); the plan plans Boule again then, as it does for the customers organisation.
|
|
98
|
+
*/
|
|
99
|
+
export declare const BOULE_TEST_CUSTOMERS_ORG_ENV = "TEST_CUSTOMERS_ORGANISATION_ID";
|
|
94
100
|
/**
|
|
95
101
|
* When the Archon credential was last minted and held (ISO date-time). No secret. Archon's mint rotates
|
|
96
102
|
* on every call and the credential lives 365 days, so foundry mints only when none was pushed or the
|
|
@@ -130,9 +136,25 @@ export interface BouleCredentialWants {
|
|
|
130
136
|
customersDirectory?: boolean;
|
|
131
137
|
/** The recorded Archon credential is missing or 335 days old: the step mints it again. */
|
|
132
138
|
archonRenewDue?: boolean;
|
|
139
|
+
/** F13: Boule is given `CUSTOMER_GRANTS` (an entry declares `uses.customerGrants`; needs `customersDirectory`). */
|
|
140
|
+
customerGrants?: boolean;
|
|
141
|
+
/** T3: Boule is given its company's registrar key at the sign-in broker (`plan/broker-registrar.ts`). */
|
|
142
|
+
registrar?: boolean;
|
|
143
|
+
/** T3b: customers are off but the broker still holds the record, so Boule keeps its registrar env (plan only). */
|
|
144
|
+
registrarKept?: boolean;
|
|
133
145
|
}
|
|
146
|
+
/** T3: the broker's base and the company's registrar key, in Boule's env (`plan/broker-registrar.ts`). */
|
|
147
|
+
export declare const AUTH_REGISTRAR_URL_ENV = "AUTH_REGISTRAR_URL";
|
|
148
|
+
export declare const AUTH_REGISTRAR_KEY_ENV = "AUTH_REGISTRAR_KEY";
|
|
149
|
+
/**
|
|
150
|
+
* A company gets a registrar while it has customers (the people its own apps sign in) or its Boule runs apps
|
|
151
|
+
* (F5: any company that installs an app with sign-in, `org.boule.apps`).
|
|
152
|
+
*/
|
|
153
|
+
export declare const registrarWanted: (org: Pick<DeclaredOrg, "customers" | "boule">) => boolean;
|
|
154
|
+
/** The broker's base as Boule calls it: `@wtfalch/auth`'s own `brokerUrl` default, `<issuer>/api`. */
|
|
155
|
+
export declare const registrarUrl: (issuer: string) => string;
|
|
134
156
|
/** The env names `desiredBouleEnv` can write, so the live reader knows which to read back. */
|
|
135
|
-
export declare const BOULE_MANAGED_ENV_KEYS: readonly ["APP_URL", "AUTH_ISSUER", "AUTH_ORGANIZATION_ID", "AUTH_CLIENT_ID", "BOULE_ORG_NAME", "AUTHZ_BOOTSTRAP_OPERATOR", "AUTHZ_FIRST_OWNER", "ARCHON_URL", "MAIL_FROM", "LETTERMINT_API_TOKEN", "AUTHZ_PEER_PASSWORD_OPERATOR", "ARCHON_BRIDGE_SECRET", "OWNER_SEED_SECRET", "ARCHON_ORG_ID", "ARCHON_CREDENTIAL", "CUSTOMERS_ORGANISATION_ID", "CUSTOMERS_DIRECTORY_CREDENTIAL", "SIGNIN_UPSTREAM"];
|
|
157
|
+
export declare const BOULE_MANAGED_ENV_KEYS: readonly ["APP_URL", "AUTH_ISSUER", "AUTH_ORGANIZATION_ID", "AUTH_CLIENT_ID", "BOULE_ORG_NAME", "AUTHZ_BOOTSTRAP_OPERATOR", "AUTHZ_FIRST_OWNER", "ARCHON_URL", "MAIL_FROM", "LETTERMINT_API_TOKEN", "AUTHZ_PEER_PASSWORD_OPERATOR", "ARCHON_BRIDGE_SECRET", "OWNER_SEED_SECRET", "ARCHON_ORG_ID", "ARCHON_CREDENTIAL", "CUSTOMERS_ORGANISATION_ID", "CUSTOMERS_DIRECTORY_CREDENTIAL", "TEST_CUSTOMERS_ORGANISATION_ID", "CUSTOMER_GRANTS", "SIGNIN_UPSTREAM", "AUTH_REGISTRAR_URL", "AUTH_REGISTRAR_KEY"];
|
|
136
158
|
/**
|
|
137
159
|
* The env vars an org's Boule app should carry, and every input still missing
|
|
138
160
|
* to build them. Callers that write must refuse on a non-empty `missing`: a
|
package/dist/plan/boule.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { DEFAULT_NO_REPLY_LOCAL_PART, ownerMailboxes, } from '../org.js';
|
|
1
|
+
import { DEFAULT_NO_REPLY_LOCAL_PART, appsOn, ownerMailboxes, } from '../org.js';
|
|
2
2
|
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
3
3
|
import { SIGN_IN_UPSTREAM_ENV, inAppHostApplied, signInUpstream, staleSignInUpstream, } from './sign-in.js';
|
|
4
|
+
import { testCustomersOrgKey } from './workspace-client.js';
|
|
4
5
|
/**
|
|
5
6
|
* #161 (Boule part): what both the planner (`diff.ts`) and the executor
|
|
6
7
|
* (`executor.ts`) need to agree on for an org's Boule -- the hostname, the
|
|
@@ -102,6 +103,12 @@ export const BOULE_ARCHON_CREDENTIAL_ENV = 'ARCHON_CREDENTIAL';
|
|
|
102
103
|
/** The company's customers organisation in the sign-in service, and the read-only credential that lists its people (C3). */
|
|
103
104
|
export const BOULE_CUSTOMERS_ORG_ENV = 'CUSTOMERS_ORGANISATION_ID';
|
|
104
105
|
export const BOULE_CUSTOMERS_CREDENTIAL_ENV = 'CUSTOMERS_DIRECTORY_CREDENTIAL';
|
|
106
|
+
/**
|
|
107
|
+
* F5 (Q44): the company's test customers organisation, beside {@link BOULE_CUSTOMERS_ORG_ENV}, so a workspace copy
|
|
108
|
+
* of an app registers its sign-in against it. Written once the organisation is recorded (a server workspace with
|
|
109
|
+
* customers makes it); the plan plans Boule again then, as it does for the customers organisation.
|
|
110
|
+
*/
|
|
111
|
+
export const BOULE_TEST_CUSTOMERS_ORG_ENV = 'TEST_CUSTOMERS_ORGANISATION_ID';
|
|
105
112
|
/**
|
|
106
113
|
* When the Archon credential was last minted and held (ISO date-time). No secret. Archon's mint rotates
|
|
107
114
|
* on every call and the credential lives 365 days, so foundry mints only when none was pushed or the
|
|
@@ -144,6 +151,16 @@ export const bouleArchonCredentialRef = (orgId) => `foundry:boule-archon:${orgId
|
|
|
144
151
|
export const customersDirectoryRef = (orgId) => `foundry:customers-directory:${orgId}`;
|
|
145
152
|
/** The customers organisation id `zitadel:customers` recorded. */
|
|
146
153
|
export const customersOrgIdOf = (org, recorded) => recorded[`zitadel:customers:${org.id}`] || undefined;
|
|
154
|
+
/** T3: the broker's base and the company's registrar key, in Boule's env (`plan/broker-registrar.ts`). */
|
|
155
|
+
export const AUTH_REGISTRAR_URL_ENV = 'AUTH_REGISTRAR_URL';
|
|
156
|
+
export const AUTH_REGISTRAR_KEY_ENV = 'AUTH_REGISTRAR_KEY';
|
|
157
|
+
/**
|
|
158
|
+
* A company gets a registrar while it has customers (the people its own apps sign in) or its Boule runs apps
|
|
159
|
+
* (F5: any company that installs an app with sign-in, `org.boule.apps`).
|
|
160
|
+
*/
|
|
161
|
+
export const registrarWanted = (org) => org.customers?.enabled === true || appsOn(org);
|
|
162
|
+
/** The broker's base as Boule calls it: `@wtfalch/auth`'s own `brokerUrl` default, `<issuer>/api`. */
|
|
163
|
+
export const registrarUrl = (issuer) => `${issuer.replace(/\/+$/, '')}/api`;
|
|
147
164
|
/** The env names `desiredBouleEnv` can write, so the live reader knows which to read back. */
|
|
148
165
|
export const BOULE_MANAGED_ENV_KEYS = [
|
|
149
166
|
'APP_URL',
|
|
@@ -163,7 +180,11 @@ export const BOULE_MANAGED_ENV_KEYS = [
|
|
|
163
180
|
BOULE_ARCHON_CREDENTIAL_ENV,
|
|
164
181
|
BOULE_CUSTOMERS_ORG_ENV,
|
|
165
182
|
BOULE_CUSTOMERS_CREDENTIAL_ENV,
|
|
183
|
+
BOULE_TEST_CUSTOMERS_ORG_ENV,
|
|
184
|
+
'CUSTOMER_GRANTS',
|
|
166
185
|
SIGN_IN_UPSTREAM_ENV,
|
|
186
|
+
AUTH_REGISTRAR_URL_ENV,
|
|
187
|
+
AUTH_REGISTRAR_KEY_ENV,
|
|
167
188
|
];
|
|
168
189
|
/**
|
|
169
190
|
* The env vars an org's Boule app should carry, and every input still missing
|
|
@@ -218,6 +239,14 @@ export function desiredBouleEnv(org, recorded, issuer, archonUrl, mailWanted = f
|
|
|
218
239
|
const customersOrgId = wants.customersDirectory ? customersOrgIdOf(org, recorded) : undefined;
|
|
219
240
|
if (customersOrgId)
|
|
220
241
|
env[BOULE_CUSTOMERS_ORG_ENV] = customersOrgId;
|
|
242
|
+
const testCustomersOrgId = wants.customersDirectory
|
|
243
|
+
? recorded[testCustomersOrgKey(org.id)]
|
|
244
|
+
: undefined;
|
|
245
|
+
if (testCustomersOrgId)
|
|
246
|
+
env[BOULE_TEST_CUSTOMERS_ORG_ENV] = testCustomersOrgId;
|
|
247
|
+
// The key itself is a secret the executor writes; the plan compares it by presence (`bouleRegistrarDrift`).
|
|
248
|
+
if (wants.registrar && issuer)
|
|
249
|
+
env[AUTH_REGISTRAR_URL_ENV] = registrarUrl(issuer);
|
|
221
250
|
const owner = org.owners[0];
|
|
222
251
|
const ownerUserId = owner
|
|
223
252
|
? recorded[`zitadel:owner:${org.id}:${owner.email.toLowerCase()}`]
|
|
@@ -355,5 +384,9 @@ export function planBouleOwners(org, recorded, liveBouleEnv) {
|
|
|
355
384
|
export function staleCustomersEnv(org, live) {
|
|
356
385
|
if (org.customers?.enabled)
|
|
357
386
|
return [];
|
|
358
|
-
return [
|
|
387
|
+
return [
|
|
388
|
+
BOULE_CUSTOMERS_ORG_ENV,
|
|
389
|
+
BOULE_TEST_CUSTOMERS_ORG_ENV,
|
|
390
|
+
BOULE_CUSTOMERS_CREDENTIAL_ENV,
|
|
391
|
+
].filter((key) => live[key] !== undefined);
|
|
359
392
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DeclaredOrg, type HostingEntry } from '../org.js';
|
|
2
|
-
import type { DeclaredOrgStore, PlatformStore } from '../store.js';
|
|
2
|
+
import type { DeclaredOrgStore, HeldLease, MutationLease, PlatformStore } from '../store.js';
|
|
3
3
|
import type { CoolifyPort } from './executor.js';
|
|
4
4
|
/**
|
|
5
5
|
* F5 (https://github.com/wtfalch/foundry/issues/314): an entry with
|
|
@@ -62,6 +62,11 @@ export interface BrokerEntryWant {
|
|
|
62
62
|
loginHost: string;
|
|
63
63
|
onboardingUrl?: string;
|
|
64
64
|
};
|
|
65
|
+
/** Present only for an entry with `uses.auth.workspaces`. The key's hash is not part of it: see {@link writeBrokerEntry}. */
|
|
66
|
+
workspaces?: {
|
|
67
|
+
roles: string[];
|
|
68
|
+
inviteUrl: string;
|
|
69
|
+
};
|
|
65
70
|
/**
|
|
66
71
|
* `true` for `signIn: 'email-code'`, `false` for an entry that declares `signIn: 'password'`.
|
|
67
72
|
* Absent when the entry declares no `signIn`: a hand-set `emailLink` stays as it is.
|
|
@@ -95,10 +100,22 @@ export interface BrokerPort {
|
|
|
95
100
|
is_preview: boolean;
|
|
96
101
|
}>>;
|
|
97
102
|
setAuthApps(raw: string): Promise<void>;
|
|
103
|
+
/** Sets one env of the broker app. Needed only to give the broker its `WORKSPACES_DATABASE_URL`. */
|
|
104
|
+
setEnv?(key: string, value: string): Promise<void>;
|
|
98
105
|
deploy(): Promise<void>;
|
|
99
106
|
/** The platform's `PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY` value, if any. */
|
|
100
107
|
recordedDeploy(): Promise<string | undefined>;
|
|
101
|
-
|
|
108
|
+
/** With `fence` (a `platform` lease the caller holds, from {@link withPlatformLease}) the write carries it and takes no lease itself. */
|
|
109
|
+
recordDeploy(value: string, fence?: MutationLease): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* T3b: runs `fn` holding the `platform` mutation lease, so two companies' applies never read, change and
|
|
112
|
+
* write a shared broker env (`AUTH_REGISTRARS`) at once. `fn` gets the lease (undefined for a port with
|
|
113
|
+
* no lease store), which its `recordDeploy` carries. Returns {@link HeldLease} while another holder has
|
|
114
|
+
* it, and `fn` does not run. Optional: a port without it writes unleased.
|
|
115
|
+
*/
|
|
116
|
+
withPlatformLease?<T>(fn: (fence: MutationLease | undefined) => Promise<T>): Promise<{
|
|
117
|
+
value: T;
|
|
118
|
+
} | HeldLease>;
|
|
102
119
|
/**
|
|
103
120
|
* The id of Archon's own sign-in organisation, when the platform has made it.
|
|
104
121
|
* No other entry may sign in that organisation, so an entry that does is
|
|
@@ -129,10 +146,14 @@ export declare class PlatformBrokerPort implements BrokerPort {
|
|
|
129
146
|
is_preview: boolean;
|
|
130
147
|
}[]>;
|
|
131
148
|
setAuthApps(raw: string): Promise<void>;
|
|
149
|
+
setEnv(key: string, value: string): Promise<void>;
|
|
132
150
|
deploy(): Promise<void>;
|
|
133
151
|
recordedDeploy(): Promise<string | undefined>;
|
|
134
152
|
archonOrganisationId(): Promise<string | undefined>;
|
|
135
|
-
|
|
153
|
+
withPlatformLease<T>(fn: (fence: MutationLease | undefined) => Promise<T>): Promise<{
|
|
154
|
+
value: T;
|
|
155
|
+
} | HeldLease>;
|
|
156
|
+
recordDeploy(value: string, fence?: MutationLease): Promise<void>;
|
|
136
157
|
}
|
|
137
158
|
/**
|
|
138
159
|
* Writes one entry's broker entry beside the others and deploys the broker
|
|
@@ -149,6 +170,10 @@ export declare function writeBrokerEntry(broker: BrokerPort, input: {
|
|
|
149
170
|
want: BrokerEntryWant;
|
|
150
171
|
appKey: string;
|
|
151
172
|
previous: BrokerEntryWant | null;
|
|
173
|
+
/** sha256 hex of the entry's workspaces key. Required with `want.workspaces`; the key itself never reaches here. */
|
|
174
|
+
workspacesKeyHash?: string;
|
|
175
|
+
/** The operator's held connection string for the broker's workspaces database, written to the broker when it differs. */
|
|
176
|
+
workspacesDatabaseUrl?: string;
|
|
152
177
|
}): Promise<{
|
|
153
178
|
invalidForeign: string[];
|
|
154
179
|
replaced: string | null;
|