@wtfalch/foundry 0.60.0 → 0.62.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 +14 -5
- package/dist/index.js +7 -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/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -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/native-build.d.ts +70 -0
- package/dist/native-build.js +92 -0
- package/dist/org.d.ts +184 -24
- package/dist/org.js +444 -49
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- 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 +60 -8
- package/dist/plan/blue-green.js +115 -10
- 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/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +15 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +552 -104
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-access.js +1 -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 +231 -19
- package/dist/plan/executor.js +1648 -129
- 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/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +7 -4
- 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.d.ts +3 -3
- package/dist/plan/org-names.js +15 -2
- package/dist/plan/org-postgres-migrate.js +1 -0
- 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-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -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 +13 -2
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +12 -3
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/providers-readers.js +20 -1
- 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/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- 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 +13 -1
- 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 +20 -7
- package/dist/store/postgres.js +25 -2
- package/dist/store/schema.d.ts +73 -1
- package/dist/store/schema.js +10 -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 +19 -2
|
@@ -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])),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BlueGreenConfig, type DeclaredOrg, type HostingEntry } from '../org.js';
|
|
2
2
|
import { type StageKind } from './stage-isolation.js';
|
|
3
3
|
/**
|
|
4
4
|
* Blue/green releases (docs/adr/0001-blue-green-releases.md). A color is a property any app can have
|
|
@@ -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. */
|
|
@@ -133,11 +164,14 @@ export interface BlueGreenSubject {
|
|
|
133
164
|
* (address check, crash loop) and moves up by time.
|
|
134
165
|
*/
|
|
135
166
|
reporting: boolean;
|
|
136
|
-
/** Set for Boule and
|
|
167
|
+
/** Set for Boule, Agora and kosmos, which are not hosting entries. */
|
|
137
168
|
builtin?: BuiltinApp;
|
|
138
169
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
170
|
+
/**
|
|
171
|
+
* The apps foundry runs for a company that can declare blue/green: Boule and Agora, every company's own, plus
|
|
172
|
+
* kosmos once the company turns it on (`org.boule`, `org.agora`, `org.kosmos`; docs/adr/0001, rule 1).
|
|
173
|
+
*/
|
|
174
|
+
export type BuiltinApp = 'boule' | 'agora' | 'kosmos';
|
|
141
175
|
export declare const BUILTIN_APPS: readonly BuiltinApp[];
|
|
142
176
|
/** What foundry builds each built-in app from; `executor.ts` creates the app from the same values. */
|
|
143
177
|
export declare const BUILTIN_APP: {
|
|
@@ -157,6 +191,14 @@ export declare const BUILTIN_APP: {
|
|
|
157
191
|
readonly database: false;
|
|
158
192
|
readonly reporting: false;
|
|
159
193
|
};
|
|
194
|
+
readonly kosmos: {
|
|
195
|
+
readonly repo: "wtfalch/kosmos";
|
|
196
|
+
readonly dockerfile: "/Dockerfile";
|
|
197
|
+
readonly port: 3000;
|
|
198
|
+
readonly healthPath: "/api/health";
|
|
199
|
+
readonly database: true;
|
|
200
|
+
readonly reporting: true;
|
|
201
|
+
};
|
|
160
202
|
};
|
|
161
203
|
/**
|
|
162
204
|
* A hosting entry that declares `blueGreen`, as a subject; `null` when it does not or cannot run.
|
|
@@ -164,11 +206,12 @@ export declare const BUILTIN_APP: {
|
|
|
164
206
|
*/
|
|
165
207
|
export declare function entrySubject(org: DeclaredOrg, entry: HostingEntry, recorded?: Readonly<Record<string, string>>): BlueGreenSubject | null;
|
|
166
208
|
/**
|
|
167
|
-
* The company's Boule or
|
|
168
|
-
* its name `<slug>-<app>` and is blue, like
|
|
209
|
+
* The company's Boule, Agora or kosmos as a subject; `null` when it does not declare blue/green (see
|
|
210
|
+
* {@link builtinDeclaresBlueGreen}). The running app keeps its name `<slug>-<app>` and is blue, like
|
|
211
|
+
* platform-archon. All three run on the primary server only.
|
|
169
212
|
*/
|
|
170
213
|
export declare function builtinSubject(org: DeclaredOrg, app: BuiltinApp, recorded?: Readonly<Record<string, string>>): BlueGreenSubject | null;
|
|
171
|
-
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule and
|
|
214
|
+
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule, Agora and kosmos. */
|
|
172
215
|
export declare function blueGreenSubjects(org: DeclaredOrg, recorded?: Readonly<Record<string, string>>): BlueGreenSubject[];
|
|
173
216
|
/** Whether the org declares blue/green for any of its apps. */
|
|
174
217
|
export declare const declaresBlueGreen: (org: DeclaredOrg) => boolean;
|
|
@@ -200,6 +243,10 @@ export declare const LB_COOKIE_NAME = "bg_color";
|
|
|
200
243
|
export declare const LB_COLOR_HEADER = "X-Foundry-Color";
|
|
201
244
|
/** A pinned request also carries this header with the entry's pin key, so a stranger cannot pin a color. */
|
|
202
245
|
export declare const LB_PIN_HEADER = "X-Foundry-Pin";
|
|
246
|
+
/** The cookie that sends a signed-in person with `releases:preview` to the idle slot. */
|
|
247
|
+
export declare const LB_PREVIEW_COOKIE = "bg_preview";
|
|
248
|
+
/** The path the balancer asks the live slot whether a request may see the idle slot. */
|
|
249
|
+
export declare const LB_PREVIEW_CHECK_PATH = "/api/foundry/preview-check";
|
|
203
250
|
export declare const LB_CONFIG_PATH = "/etc/traefik/dynamic/lb.yml";
|
|
204
251
|
/**
|
|
205
252
|
* The balancer's inline Dockerfile: a pinned Traefik with a file provider watching one directory.
|
|
@@ -218,6 +265,10 @@ export declare function renderLbConfig(input: {
|
|
|
218
265
|
weights: Weights;
|
|
219
266
|
backends: Record<Color, string[]>;
|
|
220
267
|
pinKey: string;
|
|
268
|
+
/** The slot that is not live: the `preview` router sends a cookie holder here. */
|
|
269
|
+
idle: Color;
|
|
270
|
+
/** True when the subject has a server that can answer the permission check; a static subject gets no router. */
|
|
271
|
+
reporting: boolean;
|
|
221
272
|
}): string;
|
|
222
273
|
/**
|
|
223
274
|
* The URL of one color's container on its own server's network. On the primary, blue is the app that was
|
|
@@ -324,3 +375,4 @@ export declare function planArchonBlueGreen(input: {
|
|
|
324
375
|
step: BlueGreenPlanStep | null;
|
|
325
376
|
unmanaged: UnmanagedLine[];
|
|
326
377
|
};
|
|
378
|
+
export {};
|
package/dist/plan/blue-green.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { VERSIONS, pinnedRef } from '../bootstrap/versions.js';
|
|
3
|
+
import { kosmosOn } from '../org.js';
|
|
3
4
|
import { agoraHostnameFor } from './agora.js';
|
|
4
5
|
import { bouleHostnameFor } from './boule.js';
|
|
5
6
|
import { serverConnectorScopeKey } from './cloudflared-connector.js';
|
|
6
7
|
import { entryHostnames, tunnelIngressHostnames, wildcardHostname } from './hostnames.js';
|
|
8
|
+
import { kosmosHostnameFor } from './kosmos.js';
|
|
7
9
|
import { appNameFor, appStepKey } from './platform-diff.js';
|
|
8
10
|
import { remoteDatabaseAppliedFor } from './remote-database.js';
|
|
9
11
|
import { buildStageEnv } from './stage-env.js';
|
|
@@ -55,7 +57,10 @@ export const bgKey = {
|
|
|
55
57
|
stageState: (id) => `bg.${id}.stagestate`,
|
|
56
58
|
/** The Coolify name of the blue app on the primary: the adopted single app's name, or `<base>-blue`. */
|
|
57
59
|
blueName: (id) => `bg.${id}.bluename`,
|
|
58
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The held ref of the balancer's pin key (a credential: it lets a caller pick a color). A value that is 64 hex
|
|
62
|
+
* characters is the older recorded key, moved to the held ref on the next read and then overwritten by the ref.
|
|
63
|
+
*/
|
|
59
64
|
pinKey: (id) => `bg.${id}.pinkey`,
|
|
60
65
|
/** The hash of the stage env name set and resource set that passed the isolation check. */
|
|
61
66
|
stageIsolation: (id) => `bg.${id}.stageisolation`,
|
|
@@ -102,6 +107,56 @@ export async function mintActionsKey(input) {
|
|
|
102
107
|
await input.persist();
|
|
103
108
|
return key;
|
|
104
109
|
}
|
|
110
|
+
/** The held-key name of a subject's balancer pin key: one mint per subject. */
|
|
111
|
+
export const pinKeyName = (id) => `bg-pin-${id}`;
|
|
112
|
+
/**
|
|
113
|
+
* The balancer's pin key, held (CLAUDE.md, Platform steps, rules 1 and 6). Order: the held key; else the older
|
|
114
|
+
* recorded key (64 hex), put in the held store and replaced by the ref; else one mint, put first, then the ref is
|
|
115
|
+
* recorded and persisted. A crash between the put and the record reuses the held key. A recorded ref whose key is
|
|
116
|
+
* no longer held stops: it never mints again. Errors name the ref only.
|
|
117
|
+
*/
|
|
118
|
+
export async function mintPinKey(input) {
|
|
119
|
+
const held = await input.heldKeys.resolve(input.ref);
|
|
120
|
+
const recordedValue = input.recorded()[bgKey.pinKey(input.id)];
|
|
121
|
+
if (held) {
|
|
122
|
+
input.secret(held);
|
|
123
|
+
if (recordedValue !== input.ref) {
|
|
124
|
+
input.record(bgKey.pinKey(input.id), input.ref);
|
|
125
|
+
await input.persist();
|
|
126
|
+
}
|
|
127
|
+
return held;
|
|
128
|
+
}
|
|
129
|
+
const put = input.heldKeys.put?.bind(input.heldKeys);
|
|
130
|
+
if (!put)
|
|
131
|
+
throw new Error(`pin key ${input.ref} cannot be held: the host has no held store to write`);
|
|
132
|
+
if (recordedValue && PIN_KEY.test(recordedValue)) {
|
|
133
|
+
input.secret(recordedValue);
|
|
134
|
+
await put(input.ref, recordedValue);
|
|
135
|
+
input.record(bgKey.pinKey(input.id), input.ref);
|
|
136
|
+
await input.persist();
|
|
137
|
+
return recordedValue;
|
|
138
|
+
}
|
|
139
|
+
if (recordedValue) {
|
|
140
|
+
throw new Error(`pin key ${input.ref} was minted but is no longer held; restore the held value`);
|
|
141
|
+
}
|
|
142
|
+
const key = input.generate();
|
|
143
|
+
input.secret(key);
|
|
144
|
+
await put(input.ref, key);
|
|
145
|
+
input.record(bgKey.pinKey(input.id), input.ref);
|
|
146
|
+
await input.persist();
|
|
147
|
+
return key;
|
|
148
|
+
}
|
|
149
|
+
/** Reads the pin key without minting: the held key, else the older recorded key, else null. */
|
|
150
|
+
export async function readPinKey(input) {
|
|
151
|
+
const held = await input.heldKeys.resolve(input.ref);
|
|
152
|
+
const value = held ||
|
|
153
|
+
(PIN_KEY.test(input.recorded()[bgKey.pinKey(input.id)] ?? '')
|
|
154
|
+
? input.recorded()[bgKey.pinKey(input.id)]
|
|
155
|
+
: null);
|
|
156
|
+
if (value)
|
|
157
|
+
input.secret(value);
|
|
158
|
+
return value ?? null;
|
|
159
|
+
}
|
|
105
160
|
/** The held-key name of an entry's health token (`sharedSecretRef(org.id, name)`): one mint per entry. */
|
|
106
161
|
export const healthTokenName = (id) => `reporting-health-${id}`;
|
|
107
162
|
/** What a color's container gets for the reporting health read. The release is left out until a digest is known. */
|
|
@@ -112,7 +167,7 @@ export function reportingEnv(input) {
|
|
|
112
167
|
{ key: REPORTING_ENV.healthToken, value: input.healthToken },
|
|
113
168
|
];
|
|
114
169
|
}
|
|
115
|
-
export const BUILTIN_APPS = ['boule', 'agora'];
|
|
170
|
+
export const BUILTIN_APPS = ['boule', 'agora', 'kosmos'];
|
|
116
171
|
/** What foundry builds each built-in app from; `executor.ts` creates the app from the same values. */
|
|
117
172
|
export const BUILTIN_APP = {
|
|
118
173
|
boule: {
|
|
@@ -132,7 +187,31 @@ export const BUILTIN_APP = {
|
|
|
132
187
|
database: false,
|
|
133
188
|
reporting: false,
|
|
134
189
|
},
|
|
190
|
+
kosmos: {
|
|
191
|
+
repo: 'wtfalch/kosmos',
|
|
192
|
+
dockerfile: '/Dockerfile',
|
|
193
|
+
port: 3000,
|
|
194
|
+
healthPath: '/api/health',
|
|
195
|
+
database: true,
|
|
196
|
+
reporting: true,
|
|
197
|
+
},
|
|
135
198
|
};
|
|
199
|
+
/** `builtinSubject`'s hostname for each built-in app, kept beside {@link BUILTIN_APP} rather than a third switch. */
|
|
200
|
+
function builtinHostnameFor(org, app) {
|
|
201
|
+
if (app === 'boule')
|
|
202
|
+
return bouleHostnameFor(org);
|
|
203
|
+
if (app === 'agora')
|
|
204
|
+
return agoraHostnameFor(org);
|
|
205
|
+
return kosmosHostnameFor(org);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Whether `org` declares blue/green for a built-in app: Boule and Agora opt into it through their own
|
|
209
|
+
* `blueGreen` field (they already run as a single copy otherwise); kosmos has no single-copy mode -- turning
|
|
210
|
+
* it on (`org.kosmos.enabled`) always runs it blue/green, docs/adr/0001 rule 1.
|
|
211
|
+
*/
|
|
212
|
+
function builtinDeclaresBlueGreen(org, app) {
|
|
213
|
+
return app === 'kosmos' ? kosmosOn(org) : org[app]?.blueGreen !== undefined;
|
|
214
|
+
}
|
|
136
215
|
/**
|
|
137
216
|
* A hosting entry that declares `blueGreen`, as a subject; `null` when it does not or cannot run.
|
|
138
217
|
* `recorded` names the blue app: an app that ran before blue/green keeps its own name (see {@link liveAppNameFor}).
|
|
@@ -160,13 +239,14 @@ export function entrySubject(org, entry, recorded) {
|
|
|
160
239
|
};
|
|
161
240
|
}
|
|
162
241
|
/**
|
|
163
|
-
* The company's Boule or
|
|
164
|
-
* its name `<slug>-<app>` and is blue, like
|
|
242
|
+
* The company's Boule, Agora or kosmos as a subject; `null` when it does not declare blue/green (see
|
|
243
|
+
* {@link builtinDeclaresBlueGreen}). The running app keeps its name `<slug>-<app>` and is blue, like
|
|
244
|
+
* platform-archon. All three run on the primary server only.
|
|
165
245
|
*/
|
|
166
246
|
export function builtinSubject(org, app, recorded = {}) {
|
|
167
|
-
|
|
168
|
-
if (!config)
|
|
247
|
+
if (!builtinDeclaresBlueGreen(org, app))
|
|
169
248
|
return null;
|
|
249
|
+
const config = org[app]?.blueGreen ?? {};
|
|
170
250
|
const spec = BUILTIN_APP[app];
|
|
171
251
|
const base = `${org.slug}-${app}`;
|
|
172
252
|
return {
|
|
@@ -180,7 +260,7 @@ export function builtinSubject(org, app, recorded = {}) {
|
|
|
180
260
|
buildPack: 'dockerfile',
|
|
181
261
|
healthPath: spec.healthPath,
|
|
182
262
|
database: spec.database,
|
|
183
|
-
hostnames: [
|
|
263
|
+
hostnames: [builtinHostnameFor(org, app)],
|
|
184
264
|
config,
|
|
185
265
|
blueAppKey: `coolify:${app}:${org.id}`,
|
|
186
266
|
dockerfileLocation: spec.dockerfile,
|
|
@@ -188,7 +268,7 @@ export function builtinSubject(org, app, recorded = {}) {
|
|
|
188
268
|
builtin: app,
|
|
189
269
|
};
|
|
190
270
|
}
|
|
191
|
-
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule and
|
|
271
|
+
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule, Agora and kosmos. */
|
|
192
272
|
export function blueGreenSubjects(org, recorded) {
|
|
193
273
|
const subjects = [];
|
|
194
274
|
for (const entry of org.hosting.entries) {
|
|
@@ -205,7 +285,7 @@ export function blueGreenSubjects(org, recorded) {
|
|
|
205
285
|
}
|
|
206
286
|
/** Whether the org declares blue/green for any of its apps. */
|
|
207
287
|
export const declaresBlueGreen = (org) => org.hosting.entries.some((entry) => entry.blueGreen) ||
|
|
208
|
-
BUILTIN_APPS.some((app) => org
|
|
288
|
+
BUILTIN_APPS.some((app) => builtinDeclaresBlueGreen(org, app));
|
|
209
289
|
/**
|
|
210
290
|
* The Coolify name of the entry's app on the primary. A blue/green entry's app is its blue copy: the recorded
|
|
211
291
|
* name; else the entry's own name when its app is already recorded (it ran before blue/green and is adopted as
|
|
@@ -246,6 +326,11 @@ export const LB_COLOR_HEADER = 'X-Foundry-Color';
|
|
|
246
326
|
/** A pinned request also carries this header with the entry's pin key, so a stranger cannot pin a color. */
|
|
247
327
|
export const LB_PIN_HEADER = 'X-Foundry-Pin';
|
|
248
328
|
const PIN_KEY = /^[0-9a-f]{64}$/;
|
|
329
|
+
/** The cookie that sends a signed-in person with `releases:preview` to the idle slot. */
|
|
330
|
+
export const LB_PREVIEW_COOKIE = 'bg_preview';
|
|
331
|
+
/** The path the balancer asks the live slot whether a request may see the idle slot. */
|
|
332
|
+
export const LB_PREVIEW_CHECK_PATH = '/api/foundry/preview-check';
|
|
333
|
+
const LB_PREVIEW_PRIORITY = 9000;
|
|
249
334
|
/** Above the default priority of `app` (its rule length), so a pinned request never reaches the weighted service. */
|
|
250
335
|
const LB_PIN_PRIORITY = 10000;
|
|
251
336
|
export const LB_CONFIG_PATH = '/etc/traefik/dynamic/lb.yml';
|
|
@@ -272,7 +357,8 @@ export const LB_IMAGE_REF = `traefik:${VERSIONS.traefik}`;
|
|
|
272
357
|
* line, so a reader sees both numbers. `backends` are the URLs of each color's containers, one per server.
|
|
273
358
|
*/
|
|
274
359
|
export function renderLbConfig(input) {
|
|
275
|
-
const { weights, backends, pinKey } = input;
|
|
360
|
+
const { weights, backends, pinKey, idle } = input;
|
|
361
|
+
const live = COLORS.find((color) => color !== idle);
|
|
276
362
|
if (!PIN_KEY.test(pinKey))
|
|
277
363
|
throw new Error('the load balancer pin key is not 64 hex characters');
|
|
278
364
|
const listed = COLORS.filter((color) => weights[color] > 0);
|
|
@@ -287,10 +373,29 @@ export function renderLbConfig(input) {
|
|
|
287
373
|
` entryPoints: [${LB_ENTRYPOINT}]`,
|
|
288
374
|
` service: ${color}`,
|
|
289
375
|
]),
|
|
376
|
+
...(input.reporting
|
|
377
|
+
? [
|
|
378
|
+
' preview:',
|
|
379
|
+
` rule: HeaderRegexp(\`Cookie\`, \`(^|;\\s*)${LB_PREVIEW_COOKIE}=1\`)`,
|
|
380
|
+
` priority: ${LB_PREVIEW_PRIORITY}`,
|
|
381
|
+
` entryPoints: [${LB_ENTRYPOINT}]`,
|
|
382
|
+
` service: ${idle}`,
|
|
383
|
+
' middlewares: [preview-check]',
|
|
384
|
+
]
|
|
385
|
+
: []),
|
|
290
386
|
' app:',
|
|
291
387
|
' rule: PathPrefix(`/`)',
|
|
292
388
|
` entryPoints: [${LB_ENTRYPOINT}]`,
|
|
293
389
|
' service: weighted',
|
|
390
|
+
...(input.reporting
|
|
391
|
+
? [
|
|
392
|
+
' middlewares:',
|
|
393
|
+
' preview-check:',
|
|
394
|
+
' forwardAuth:',
|
|
395
|
+
` address: ${backends[live][0]}${LB_PREVIEW_CHECK_PATH}`,
|
|
396
|
+
' trustForwardHeader: false',
|
|
397
|
+
]
|
|
398
|
+
: []),
|
|
294
399
|
' services:',
|
|
295
400
|
' weighted:',
|
|
296
401
|
' 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
|
}
|