@wtfalch/foundry 0.62.0 → 0.63.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/bootstrap/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -1
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/native-build.d.ts +7 -27
- package/dist/native-build.js +7 -27
- package/dist/plan/archon-blue-green.d.ts +5 -0
- package/dist/plan/archon-blue-green.js +2 -0
- package/dist/plan/blue-green-apply.d.ts +5 -0
- package/dist/plan/blue-green-apply.js +197 -22
- package/dist/plan/blue-green.d.ts +61 -2
- package/dist/plan/blue-green.js +80 -6
- package/dist/plan/boule.d.ts +1 -1
- package/dist/plan/boule.js +2 -0
- package/dist/plan/broker-entries.d.ts +9 -0
- package/dist/plan/broker-entries.js +29 -0
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +4 -0
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +7 -0
- package/dist/plan/executor.d.ts +71 -1
- package/dist/plan/executor.js +426 -24
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/jobs-scheduler-http.d.ts +2 -0
- package/dist/plan/jobs-scheduler-http.js +11 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +51 -1
- package/dist/plan/plan-apply.js +298 -25
- package/dist/plan/platform-diff.d.ts +6 -0
- package/dist/plan/platform-diff.js +74 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +156 -1
- package/dist/plan/platform-registry-keys.d.ts +58 -0
- package/dist/plan/platform-registry-keys.js +69 -0
- package/dist/plan/platform-registry.d.ts +67 -0
- package/dist/plan/platform-registry.js +298 -0
- package/dist/plan/release-host.d.ts +31 -2
- package/dist/plan/release-host.js +114 -6
- package/dist/plan/stage-copy-host.d.ts +104 -0
- package/dist/plan/stage-copy-host.js +259 -0
- package/dist/plan/stage-copy.js +44 -0
- package/dist/plan/stage-env.d.ts +9 -0
- package/dist/plan/stage-env.js +38 -0
- package/dist/plan/stage-guards.d.ts +57 -1
- package/dist/plan/stage-guards.js +106 -0
- package/dist/plan/stage-isolation.js +56 -0
- package/dist/plan/stage-services.d.ts +93 -0
- package/dist/plan/stage-services.js +153 -0
- package/dist/plan/stage-signin.d.ts +152 -0
- package/dist/plan/stage-signin.js +133 -0
- package/dist/plan/stage-sink.d.ts +2 -0
- package/dist/plan/stage-sink.js +6 -1
- package/dist/platform.d.ts +7 -1
- package/dist/platform.js +1 -0
- package/dist/release/decide.d.ts +12 -0
- package/dist/release/decide.js +37 -2
- package/dist/release/run.d.ts +71 -1
- package/dist/release/run.js +92 -8
- package/dist/release/state.d.ts +14 -1
- package/dist/release/state.js +43 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/release/types.js +2 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +11 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
package/dist/plan/blue-green.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { VERSIONS, pinnedRef } from '../bootstrap/versions.js';
|
|
3
3
|
import { kosmosOn } from '../org.js';
|
|
4
|
-
import { agoraHostnameFor } from './agora.js';
|
|
4
|
+
import { ZITADEL_ORG_SCOPE_KEY, agoraHostnameFor } from './agora.js';
|
|
5
5
|
import { bouleHostnameFor } from './boule.js';
|
|
6
6
|
import { serverConnectorScopeKey } from './cloudflared-connector.js';
|
|
7
7
|
import { entryHostnames, tunnelIngressHostnames, wildcardHostname } from './hostnames.js';
|
|
@@ -9,6 +9,7 @@ import { kosmosHostnameFor } from './kosmos.js';
|
|
|
9
9
|
import { appNameFor, appStepKey } from './platform-diff.js';
|
|
10
10
|
import { remoteDatabaseAppliedFor } from './remote-database.js';
|
|
11
11
|
import { buildStageEnv } from './stage-env.js';
|
|
12
|
+
import { stageHostname } from './stage-guards.js';
|
|
12
13
|
import { assertStageIsolated, envClass } from './stage-isolation.js';
|
|
13
14
|
import { STAGE_SINK_VERSION } from './stage-sink.js';
|
|
14
15
|
export const COLORS = ['blue', 'green'];
|
|
@@ -40,6 +41,8 @@ export const bgKey = {
|
|
|
40
41
|
digest: (id, color) => `bg.${id}.digest.${color}`,
|
|
41
42
|
weights: (id) => `bg.${id}.weights`,
|
|
42
43
|
stage: (id) => `bg.${id}.stage`,
|
|
44
|
+
/** The last `deploy_stage`: `<ISO time>|ok|<commit>` or `<ISO time>|failed|<step>`. */
|
|
45
|
+
stageDeploy: (id) => `bg.${id}.stagedeploy`,
|
|
43
46
|
window: (id) => `bg.${id}.window`,
|
|
44
47
|
held: (id, digest) => `bg.${id}.held.${digest}`,
|
|
45
48
|
/** The weights the load balancer on `server` was last written with. */
|
|
@@ -66,8 +69,29 @@ export const bgKey = {
|
|
|
66
69
|
stageIsolation: (id) => `bg.${id}.stageisolation`,
|
|
67
70
|
/** The id of the Access gate over the stage host: staff only, no bypass, no service-token rule. */
|
|
68
71
|
stageGate: (id) => `bg.${id}.stagegate`,
|
|
72
|
+
/** The hash of the sorted email list last written to the stage gate (owners plus the people holding `releases:preview`). */
|
|
73
|
+
stageGateMembers: (id) => `bg.${id}.stagegatemembers`,
|
|
69
74
|
/** `ok:<hash>` once every apply-time probe was rejected; `notrun:<names>` or `failed:<names>` otherwise. */
|
|
70
75
|
stageProbes: (id) => `bg.${id}.stageprobes`,
|
|
76
|
+
/** Stage's own sign-in client: its ZITADEL app id, and its client id (public, not a secret). */
|
|
77
|
+
stageClient: (id) => `bg.${id}.stageclient`,
|
|
78
|
+
stageClientId: (id) => `bg.${id}.stageclientid`,
|
|
79
|
+
/** The held ref of stage's app key, recorded and persisted as soon as it is minted (no `:`: undelivered). */
|
|
80
|
+
stageSignInKey: (id) => `bg.${id}.stagesigninkey`,
|
|
81
|
+
/** `{name, organizationId}` of stage's broker entry, so org destroy can remove it. No key. */
|
|
82
|
+
stageBroker: (id) => `bg.${id}.stagebroker`,
|
|
83
|
+
/** `ok:<hash of the client, app and broker entry ids>` or `none`; the copy waits for it. */
|
|
84
|
+
stageSignIn: (id) => `bg.${id}.stagesignin`,
|
|
85
|
+
/** `none` (production's app uses no files, ai or jobs) or `ok:<hash>` once every service it uses is held by stage or listed absent. */
|
|
86
|
+
stageServices: (id) => `bg.${id}.stageservices`,
|
|
87
|
+
/** `ok:<hash of the ids>` or `absent:<service>:<reason>` for one service (`files`, `ai` or `jobs`). */
|
|
88
|
+
stageService: (id, service) => `bg.${id}.stage${service}`,
|
|
89
|
+
/** The id of stage's own organisation (files, ai) or project (jobs). An id, not a secret. */
|
|
90
|
+
stageServiceOrg: (id, service) => `bg.${id}.stage${service}org`,
|
|
91
|
+
/** The name stage's project or organisation was made under, when the service is deleted by name (jobs). */
|
|
92
|
+
stageServiceName: (id, service) => `bg.${id}.stage${service}name`,
|
|
93
|
+
/** `{ids, refs}` of stage's keys in that service: key ids and held refs, never a value. */
|
|
94
|
+
stageServiceKeys: (id, service) => `bg.${id}.stage${service}keys`,
|
|
71
95
|
/** `applied` once the stage app holds the sink's mail env. The copy refuses without it. */
|
|
72
96
|
stageSink: (id) => `bg.${id}.stagesink`,
|
|
73
97
|
/** `<ISO time>|<dump execution id>|ok`, written by a finished copy of production into stage. */
|
|
@@ -437,12 +461,42 @@ export function structureHash(subject, input) {
|
|
|
437
461
|
primary: input.primary,
|
|
438
462
|
lb: VERSIONS.traefik,
|
|
439
463
|
...(input.primary ? { stageSink: STAGE_SINK_VERSION } : {}),
|
|
464
|
+
// Only a primary has a stage, and only a present guard counts: no guard hashes as before.
|
|
465
|
+
...(input.primary && input.guards?.length ? { stageGuards: [...input.guards].sort() } : {}),
|
|
440
466
|
}))
|
|
441
467
|
.digest('hex')
|
|
442
468
|
.slice(0, 16);
|
|
443
469
|
}
|
|
444
|
-
/**
|
|
445
|
-
|
|
470
|
+
/**
|
|
471
|
+
* The kind of stage app a subject has: Boule and Agora by `builtin` (a hosting entry that happens to be keyed
|
|
472
|
+
* `boule` is still a customer app), platform-archon by its id, else a customer app (kosmos included: it has no lists of its own).
|
|
473
|
+
*/
|
|
474
|
+
export const stageKindOf = (subject) => subject.builtin === 'boule' || subject.builtin === 'agora'
|
|
475
|
+
? subject.builtin
|
|
476
|
+
: subject.id === ARCHON_SUBJECT_ID
|
|
477
|
+
? 'archon'
|
|
478
|
+
: 'customer';
|
|
479
|
+
/**
|
|
480
|
+
* The stage guards that can be made now, by name. The names go into the applied structure hash, so a guard
|
|
481
|
+
* that becomes possible after the first apply changes the hash and plans the structure step once more.
|
|
482
|
+
*/
|
|
483
|
+
export function stageGuardInputs(facts) {
|
|
484
|
+
const out = [];
|
|
485
|
+
if (facts.probesPort)
|
|
486
|
+
out.push('probes');
|
|
487
|
+
if (facts.accessPort &&
|
|
488
|
+
facts.providerId &&
|
|
489
|
+
facts.domain &&
|
|
490
|
+
facts.claimValue &&
|
|
491
|
+
facts.emails > 0 &&
|
|
492
|
+
!facts.hostRuled) {
|
|
493
|
+
out.push('gate');
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
/** The guard names as the plan's `desired.guards` carries them, and back. */
|
|
498
|
+
export const formatGuards = (guards) => [...guards].sort().join(',');
|
|
499
|
+
export const parseGuards = (value) => typeof value === 'string' && value ? value.split(',') : [];
|
|
446
500
|
/** The names stage can collide on. */
|
|
447
501
|
export function stageResources(subject) {
|
|
448
502
|
const database = `${subject.base}-stage-db`;
|
|
@@ -623,10 +677,22 @@ export function planBlueGreen(input) {
|
|
|
623
677
|
key: `bg:${id}:stage`,
|
|
624
678
|
});
|
|
625
679
|
}
|
|
680
|
+
// The guards that can be made now go into the applied hash: one that becomes possible after the first
|
|
681
|
+
// apply (a provider, a probe port, an owner) changes the hash and plans the structure step once more.
|
|
682
|
+
const stageHost = org.domain ? stageHostname(org.domain, id) : null;
|
|
683
|
+
const guards = stageGuardInputs({
|
|
684
|
+
probesPort: input.stageGuardPorts?.probes === true,
|
|
685
|
+
accessPort: input.stageGuardPorts?.access === true,
|
|
686
|
+
providerId: input.accessProviderId,
|
|
687
|
+
domain: org.domain,
|
|
688
|
+
claimValue: recordedIds[ZITADEL_ORG_SCOPE_KEY],
|
|
689
|
+
emails: org.owners.length,
|
|
690
|
+
hostRuled: (org.hosting.access ?? []).some((rule) => rule.hostname === stageHost),
|
|
691
|
+
});
|
|
626
692
|
for (const server of targets) {
|
|
627
693
|
const isPrimary = server === primary;
|
|
628
694
|
const key = `${org.id}:${id}:${server}`;
|
|
629
|
-
const hash = structureHash(subject, { primary: isPrimary });
|
|
695
|
+
const hash = structureHash(subject, { primary: isPrimary, guards });
|
|
630
696
|
if (!isPrimary && subject.database && !remoteDatabaseAppliedFor(recordedIds, id, server)) {
|
|
631
697
|
unmanaged.push({
|
|
632
698
|
description: `${org.slug}'s ${id} app has no blue/green copies on Coolify server ${server} yet: its database route to that server is not applied`,
|
|
@@ -651,6 +717,7 @@ export function planBlueGreen(input) {
|
|
|
651
717
|
primary,
|
|
652
718
|
weights: formatWeights(weights),
|
|
653
719
|
...(input.accessProviderId ? { accessProviderId: input.accessProviderId } : {}),
|
|
720
|
+
...(isPrimary && guards.length > 0 ? { guards: formatGuards(guards) } : {}),
|
|
654
721
|
},
|
|
655
722
|
});
|
|
656
723
|
}
|
|
@@ -749,7 +816,9 @@ export function planArchonBlueGreen(input) {
|
|
|
749
816
|
if (!primary || !up.includes(primary)) {
|
|
750
817
|
return blocked(`the primary Coolify server ${primary ?? '(none)'} is not usable and reachable`);
|
|
751
818
|
}
|
|
752
|
-
const
|
|
819
|
+
const guards = input.guards ?? [];
|
|
820
|
+
const done = recorded[bgKey.applied(subject.id, primary)] ===
|
|
821
|
+
structureHash(subject, { primary: true, guards }) &&
|
|
753
822
|
recorded[bgKey.lb(subject.id, primary)] === formatWeights(weights) &&
|
|
754
823
|
recorded[ARCHON_BG_ORIGIN_SCOPE_KEY] === lbOrigin(subject);
|
|
755
824
|
if (done)
|
|
@@ -761,7 +830,12 @@ export function planArchonBlueGreen(input) {
|
|
|
761
830
|
phase: 'after',
|
|
762
831
|
key: subject.id,
|
|
763
832
|
description: `run platform-archon as blue and green behind a load balancer on Coolify server ${primary}, then send its tunnel to the balancer`,
|
|
764
|
-
desired: {
|
|
833
|
+
desired: {
|
|
834
|
+
subject: subject.id,
|
|
835
|
+
primary,
|
|
836
|
+
weights: formatWeights(weights),
|
|
837
|
+
...(guards.length > 0 ? { guards: formatGuards(guards) } : {}),
|
|
838
|
+
},
|
|
765
839
|
},
|
|
766
840
|
};
|
|
767
841
|
}
|
package/dist/plan/boule.d.ts
CHANGED
|
@@ -154,7 +154,7 @@ export declare const registrarWanted: (org: Pick<DeclaredOrg, "customers" | "bou
|
|
|
154
154
|
/** The broker's base as Boule calls it: `@wtfalch/auth`'s own `brokerUrl` default, `<issuer>/api`. */
|
|
155
155
|
export declare const registrarUrl: (issuer: string) => string;
|
|
156
156
|
/** The env names `desiredBouleEnv` can write, so the live reader knows which to read back. */
|
|
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"];
|
|
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", "NATIVE_BUILD_KEY", "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"];
|
|
158
158
|
/**
|
|
159
159
|
* The env vars an org's Boule app should carry, and every input still missing
|
|
160
160
|
* to build them. Callers that write must refuse on a non-empty `missing`: a
|
package/dist/plan/boule.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NATIVE_BUILD_KEY_ENV } from '../native-build.js';
|
|
1
2
|
import { DEFAULT_NO_REPLY_LOCAL_PART, appsOn, ownerMailboxes, } from '../org.js';
|
|
2
3
|
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
3
4
|
import { SIGN_IN_UPSTREAM_ENV, inAppHostApplied, signInUpstream, staleSignInUpstream, } from './sign-in.js';
|
|
@@ -176,6 +177,7 @@ export const BOULE_MANAGED_ENV_KEYS = [
|
|
|
176
177
|
BOULE_PEER_PASSWORD_ENV,
|
|
177
178
|
BOULE_BRIDGE_SECRET_ENV,
|
|
178
179
|
BOULE_OWNER_SEED_SECRET_ENV,
|
|
180
|
+
NATIVE_BUILD_KEY_ENV,
|
|
179
181
|
BOULE_ARCHON_ORG_ID_ENV,
|
|
180
182
|
BOULE_ARCHON_CREDENTIAL_ENV,
|
|
181
183
|
BOULE_CUSTOMERS_ORG_ENV,
|
|
@@ -178,3 +178,12 @@ export declare function writeBrokerEntry(broker: BrokerPort, input: {
|
|
|
178
178
|
invalidForeign: string[];
|
|
179
179
|
replaced: string | null;
|
|
180
180
|
}>;
|
|
181
|
+
/**
|
|
182
|
+
* Takes one entry (and so its key) out of the broker's `AUTH_APPS`, matched by name and organisation, and
|
|
183
|
+
* deploys the broker when the list changed. Every other entry is copied back as it was. An entry that is
|
|
184
|
+
* already gone writes nothing. Errors name the entry, never a key.
|
|
185
|
+
*/
|
|
186
|
+
export declare function removeBrokerEntry(broker: BrokerPort, input: {
|
|
187
|
+
name: string;
|
|
188
|
+
organizationId: string;
|
|
189
|
+
}): Promise<boolean>;
|
|
@@ -373,3 +373,32 @@ export async function writeBrokerEntry(broker, input) {
|
|
|
373
373
|
replaced: replaced.length > 0 ? JSON.stringify(replaced) : null,
|
|
374
374
|
};
|
|
375
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Takes one entry (and so its key) out of the broker's `AUTH_APPS`, matched by name and organisation, and
|
|
378
|
+
* deploys the broker when the list changed. Every other entry is copied back as it was. An entry that is
|
|
379
|
+
* already gone writes nothing. Errors name the entry, never a key.
|
|
380
|
+
*/
|
|
381
|
+
export async function removeBrokerEntry(broker, input) {
|
|
382
|
+
const label = `broker entry '${input.name}'`;
|
|
383
|
+
const env = (await broker.listEnvs()).find((e) => e.key === 'AUTH_APPS' && !e.is_preview);
|
|
384
|
+
if (!env?.value)
|
|
385
|
+
return false;
|
|
386
|
+
let parsed;
|
|
387
|
+
try {
|
|
388
|
+
parsed = JSON.parse(env.value);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
throw new Error(`${label}: the broker's AUTH_APPS is not JSON, so nothing was removed`);
|
|
392
|
+
}
|
|
393
|
+
if (!Array.isArray(parsed)) {
|
|
394
|
+
throw new Error(`${label}: the broker's AUTH_APPS is not a JSON array, so nothing was removed`);
|
|
395
|
+
}
|
|
396
|
+
const next = parsed.filter((entry) => !(isObject(entry) &&
|
|
397
|
+
entry.name === input.name &&
|
|
398
|
+
entry.organizationId === input.organizationId));
|
|
399
|
+
if (next.length === parsed.length)
|
|
400
|
+
return false;
|
|
401
|
+
await broker.setAuthApps(JSON.stringify(next));
|
|
402
|
+
await broker.deploy();
|
|
403
|
+
return true;
|
|
404
|
+
}
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -150,6 +150,11 @@ export interface PlanOrgOptions {
|
|
|
150
150
|
blueGreenServers?: readonly BlueGreenServer[];
|
|
151
151
|
/** The company's stage mail sink is recorded and its live app is stopped or gone. */
|
|
152
152
|
stageSinkDown?: boolean;
|
|
153
|
+
/** Which stage-guard ports the host has ({@link PlanBlueGreenInput.stageGuardPorts}). */
|
|
154
|
+
stageGuardPorts?: {
|
|
155
|
+
probes: boolean;
|
|
156
|
+
access: boolean;
|
|
157
|
+
};
|
|
153
158
|
}
|
|
154
159
|
export interface PlanDetail {
|
|
155
160
|
orgId: OrgId;
|
package/dist/plan/diff.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VERSIONS } from '../bootstrap/versions.js';
|
|
2
|
+
import { NATIVE_BUILD_KEY_ENV } from '../native-build.js';
|
|
2
3
|
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, appsOn, companyGithubFor, heldEnvRef, kosmosOn, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownerNameProblem, ownsServices, } from '../org.js';
|
|
3
4
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
4
5
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, chatCallsApplied, chatHasUpstreams, desiredAgoraEnv, entryClientIdScopeKey, withRecordedMessagingProject, } from './agora.js';
|
|
@@ -568,6 +569,7 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
568
569
|
canHoldSecrets: platformReadiness.canHoldSecrets === true,
|
|
569
570
|
accessProviderId: platformReadiness.accessProviderId ?? null,
|
|
570
571
|
...(options.stageSinkDown ? { stageSinkDown: true } : {}),
|
|
572
|
+
...(options.stageGuardPorts ? { stageGuardPorts: options.stageGuardPorts } : {}),
|
|
571
573
|
appPlanned: (entryKey) => plannedSteps.some((p) => p.action.system === 'coolify' &&
|
|
572
574
|
p.action.kind === 'app' &&
|
|
573
575
|
p.action.key === appStepKey(org, entryKey)),
|
|
@@ -3386,6 +3388,8 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
3386
3388
|
drift.push(BOULE_BRIDGE_SECRET_ENV);
|
|
3387
3389
|
if (!liveEnv[BOULE_OWNER_SEED_SECRET_ENV])
|
|
3388
3390
|
drift.push(BOULE_OWNER_SEED_SECRET_ENV);
|
|
3391
|
+
if (!liveEnv[NATIVE_BUILD_KEY_ENV])
|
|
3392
|
+
drift.push(NATIVE_BUILD_KEY_ENV);
|
|
3389
3393
|
// Secrets written but Boule not restarted since: plan the step so it restarts, minting nothing.
|
|
3390
3394
|
if (platformOwner && !recordedIds[BOULE_SECRETS_RESTARTED_SCOPE_KEY]) {
|
|
3391
3395
|
drift.push('restart after the peer password and bridge secret');
|
|
@@ -87,9 +87,8 @@ export interface AccessPort {
|
|
|
87
87
|
}>;
|
|
88
88
|
replaceGateHostnames(gateId: string, hostnames: string[]): Promise<void>;
|
|
89
89
|
/**
|
|
90
|
-
* Replaces the whole list of people who pass a gate; nothing else about the gate changes.
|
|
91
|
-
*
|
|
92
|
-
* workspace's testers to change supplies a port that does (edge's company `gates.setPeople`).
|
|
90
|
+
* Replaces the whole list of people who pass a gate; nothing else about the gate changes. The edge adapter
|
|
91
|
+
* implements it with edge's platform `setGatePeople`; a host may supply its own port instead.
|
|
93
92
|
*/
|
|
94
93
|
setGatePeople?(gateId: string, emails: string[]): Promise<void>;
|
|
95
94
|
/** Deletes the gate with its application, policy and list; a gate already gone is done. */
|
package/dist/plan/edge-access.js
CHANGED
|
@@ -167,6 +167,13 @@ export function createEdgeAccess(client, options = {}) {
|
|
|
167
167
|
async replaceGateHostnames(gateId, hostnames) {
|
|
168
168
|
await writeSlot(() => access.replaceGateHostnames(gateId, { hostnames }));
|
|
169
169
|
},
|
|
170
|
+
...(typeof client.setGatePeople === 'function'
|
|
171
|
+
? {
|
|
172
|
+
async setGatePeople(gateId, emails) {
|
|
173
|
+
await writeSlot(() => access.setGatePeople(gateId, { emails }));
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
: {}),
|
|
170
177
|
async readiness() {
|
|
171
178
|
const { authDomain } = await access.readiness();
|
|
172
179
|
return { authDomain };
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ import type { DestroyTarget, StepDestroyer, StepExecutionContext, StepExecutor }
|
|
|
21
21
|
import type { EdgePortName } from './platform-org.js';
|
|
22
22
|
import type { RequestHeaderPort } from './platform-providers.js';
|
|
23
23
|
import { type ReleaseContext, type ReleaseHost } from './release-host.js';
|
|
24
|
-
import { type
|
|
24
|
+
import { type StageCopyHostConfig } from './stage-copy-host.js';
|
|
25
|
+
import { type ReleasePreviewMembers, type StageProbePort } from './stage-guards.js';
|
|
25
26
|
import type { WorkerAppDatabasePort } from './worker-app-database.js';
|
|
26
27
|
import { type KeysCustodyPort, type WorkerAppPort } from './worker-app.js';
|
|
27
28
|
export declare function coolifyProjectMark(org: Pick<DeclaredOrg, 'id'>): string;
|
|
@@ -1411,6 +1412,11 @@ export interface FilesOrganisationPort {
|
|
|
1411
1412
|
* the list is a no-op.
|
|
1412
1413
|
*/
|
|
1413
1414
|
ensureAllowedOrigin(organisationId: string, origin: string): Promise<void>;
|
|
1415
|
+
/**
|
|
1416
|
+
* Deletes an organisation with its keys. Optional: used only by org destroy for stage's own organisation
|
|
1417
|
+
* (`<org>-stage`); a port without it leaves that organisation, which destroy then reports as blocked.
|
|
1418
|
+
*/
|
|
1419
|
+
deleteOrganisation?(organisationId: string): Promise<void>;
|
|
1414
1420
|
}
|
|
1415
1421
|
/**
|
|
1416
1422
|
* Provisions one org's organisation on AI and switches its AI use on (#80),
|
|
@@ -1519,6 +1525,11 @@ export interface AiOrganisationPort {
|
|
|
1519
1525
|
keyId: string | null;
|
|
1520
1526
|
limitMicros: string;
|
|
1521
1527
|
}): Promise<unknown>;
|
|
1528
|
+
/**
|
|
1529
|
+
* Deletes an organisation with its keys and budget. Optional: used only by org destroy for stage's own
|
|
1530
|
+
* organisation (`<org>-stage`); a port without it leaves that organisation, which destroy then reports as blocked.
|
|
1531
|
+
*/
|
|
1532
|
+
deleteOrganisation?(organisationId: string): Promise<void>;
|
|
1522
1533
|
}
|
|
1523
1534
|
/**
|
|
1524
1535
|
* #317 (F7): the edge service's own management API
|
|
@@ -1634,6 +1645,11 @@ export interface JobsSchedulerPort {
|
|
|
1634
1645
|
secret?: string;
|
|
1635
1646
|
syncKey?: string;
|
|
1636
1647
|
}>;
|
|
1648
|
+
/**
|
|
1649
|
+
* Removes a project and its keys (`DELETE /admin/projects/<name>`). Optional: used only by org destroy for
|
|
1650
|
+
* stage's own project (`<org>-stage`); a port without it leaves that project, which destroy then reports as blocked.
|
|
1651
|
+
*/
|
|
1652
|
+
deleteProject?(name: string): Promise<void>;
|
|
1637
1653
|
}
|
|
1638
1654
|
/**
|
|
1639
1655
|
* #177: where the org steps' files, AI and jobs ports come from when the host
|
|
@@ -1746,6 +1762,18 @@ export interface FoundryStepExecutorPorts {
|
|
|
1746
1762
|
* one the probes are reported as not run, and the stage copy stays blocked.
|
|
1747
1763
|
*/
|
|
1748
1764
|
stageProbes?: StageProbePort;
|
|
1765
|
+
/**
|
|
1766
|
+
* The stage copy (`copy_prod_to_stage`, run by the release tick). The host supplies the dump download and the
|
|
1767
|
+
* backup storage; foundry builds the restore port itself and reads production's address live from Coolify.
|
|
1768
|
+
* Without it a copy request is refused with `no-copy-port`. The host needs `pg_restore` (Postgres 16) on PATH.
|
|
1769
|
+
*/
|
|
1770
|
+
stageCopy?: StageCopyHostConfig;
|
|
1771
|
+
/**
|
|
1772
|
+
* Who may open an org's stage gate besides its owners: the people holding `releases:preview` in `orgId`. Read on
|
|
1773
|
+
* each release tick (10 s limit) and when the gate is made; a failed read leaves the gate as it is. Without it
|
|
1774
|
+
* the gate keeps the owners it was made with.
|
|
1775
|
+
*/
|
|
1776
|
+
releasePreviewMembers?: ReleasePreviewMembers;
|
|
1749
1777
|
/**
|
|
1750
1778
|
* F4.ratelimits: overrides the rate limiting rule calls, which otherwise go through
|
|
1751
1779
|
* `edge.ratelimits` (see {@link EdgePort.ratelimits}). Kept for a host or a test that supplies its own.
|
|
@@ -2436,8 +2464,18 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2436
2464
|
* blue copy is not recorded. The health token is only read here, never minted.
|
|
2437
2465
|
*/
|
|
2438
2466
|
releaseHost(org: DeclaredOrg, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
|
|
2467
|
+
/**
|
|
2468
|
+
* The copy's ports for one subject. Production's database is the org's (`coolify:database:<org id>`) for an
|
|
2469
|
+
* entry and Boule's own for Boule; Agora has none, so its copy is refused with `no-production-database`.
|
|
2470
|
+
*/
|
|
2471
|
+
private stageCopyFor;
|
|
2439
2472
|
/** Whether the stage mail sink app is stopped or gone, read live. A failed read throws; the caller reads that as up. */
|
|
2440
2473
|
stageSinkDown(appId: string): Promise<boolean>;
|
|
2474
|
+
/** Which stage-guard ports this executor has, for the planner (they decide the guard names in the applied hash). */
|
|
2475
|
+
stageGuardPorts(): {
|
|
2476
|
+
probes: boolean;
|
|
2477
|
+
access: boolean;
|
|
2478
|
+
};
|
|
2441
2479
|
/** Every server Coolify lists with whether it is usable and reachable, for the planner. */
|
|
2442
2480
|
blueGreenServers(): Promise<BlueGreenServer[]>;
|
|
2443
2481
|
/**
|
|
@@ -2505,6 +2543,25 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2505
2543
|
* bypass or service-token rule there would open stage, so no gate is made and the guard stays unrecorded.
|
|
2506
2544
|
*/
|
|
2507
2545
|
private stageGuards;
|
|
2546
|
+
/**
|
|
2547
|
+
* One release tick for one subject: its recorded stage gate's list follows the people holding
|
|
2548
|
+
* `releases:preview`, owners always staying (`syncStageGateMembers`). Never throws.
|
|
2549
|
+
*/
|
|
2550
|
+
syncStageGateMembers(org: DeclaredOrg, subject: BlueGreenSubject, context: ReleaseContext): Promise<void>;
|
|
2551
|
+
/**
|
|
2552
|
+
* Stage's own files organisation, ai organisation and jobs project (`plan/stage-services.ts`), each named
|
|
2553
|
+
* for its subject (`<org id>-stage-<subject id>`, `<slug>-stage-<subject id>`: one per subject, as the recorded keys are) and built from the same ports production's steps use. A service whose port is
|
|
2554
|
+
* missing or not ready answers `available() === false`, and stage then records it absent and mints nothing.
|
|
2555
|
+
*/
|
|
2556
|
+
private stageServicesPort;
|
|
2557
|
+
/**
|
|
2558
|
+
* Stage's own sign-in (`plan/stage-signin.ts`): a client named `<production client>-stage` on production's
|
|
2559
|
+
* sign-in organisation and project with redirect URIs on the stage host only, a broker entry
|
|
2560
|
+
* `<production entry>-stage` with a key of its own, and that key's held ref. `{ required: false }` for an entry
|
|
2561
|
+
* that does not sign in. `undefined`, with one fixed log line saying why, when something it needs is missing:
|
|
2562
|
+
* the guard then stays unrecorded and the copy waits. Platform-archon is not covered yet.
|
|
2563
|
+
*/
|
|
2564
|
+
private stageSignInPort;
|
|
2508
2565
|
private coolifyApp;
|
|
2509
2566
|
/**
|
|
2510
2567
|
* #75: mounts a persistent volume at {@link LOG_DIR} on the app, unless one
|
|
@@ -2711,7 +2768,20 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2711
2768
|
private ensurePeerPassword;
|
|
2712
2769
|
/** Same rule for the secret Archon's invite form sends to Boule. */
|
|
2713
2770
|
private ensureBridgeSecret;
|
|
2771
|
+
/**
|
|
2772
|
+
* `minLength`, when given, also counts a present-but-empty value as not set -- the same guard
|
|
2773
|
+
* {@link ensureAuthAppKey} already applies for `AUTH_APP_KEY`, now reusable here instead of a
|
|
2774
|
+
* third, bare copy of this check (foundry#970, thermo note 1 in #957). Omitted, the three existing
|
|
2775
|
+
* callers below keep their original behaviour.
|
|
2776
|
+
*/
|
|
2714
2777
|
private mintIfAbsent;
|
|
2778
|
+
/**
|
|
2779
|
+
* Every company's Boule, F3/#970: `NATIVE_BUILD_KEY` is what Boule presents to Archon's
|
|
2780
|
+
* native-build route (estate#262) to ask it to dispatch `build-app.yml`. Minted once, like
|
|
2781
|
+
* {@link ensureAuthAppKey}, and never rotated; a present-but-empty value (the bug
|
|
2782
|
+
* `mintNativeBuildKey`'s own read-then-generate had, see #957) is treated as not set.
|
|
2783
|
+
*/
|
|
2784
|
+
private ensureNativeBuildKey;
|
|
2715
2785
|
/**
|
|
2716
2786
|
* `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
|
|
2717
2787
|
* only the first time (task brief), never regenerated or overwritten, so a
|