@wtfalch/foundry 0.42.1 → 0.44.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/versions.d.ts +2 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -2
- package/dist/migrations/0055_release_requests.sql +20 -0
- package/dist/migrations/0056_platform_archon_blue_green.sql +7 -0
- package/dist/migrations/0057_org_customers.sql +29 -0
- package/dist/org.d.ts +55 -0
- package/dist/org.js +90 -27
- package/dist/plan/archon-blue-green.d.ts +54 -0
- package/dist/plan/archon-blue-green.js +72 -0
- package/dist/plan/blue-green-apply.d.ts +82 -0
- package/dist/plan/blue-green-apply.js +360 -0
- package/dist/plan/blue-green.d.ts +223 -0
- package/dist/plan/blue-green.js +444 -0
- package/dist/plan/boule.d.ts +43 -2
- package/dist/plan/boule.js +48 -1
- package/dist/plan/broker-entries.d.ts +28 -0
- package/dist/plan/broker-entries.js +46 -1
- package/dist/plan/cloudflared-connector.d.ts +26 -0
- package/dist/plan/cloudflared-connector.js +37 -0
- package/dist/plan/diff.d.ts +20 -0
- package/dist/plan/diff.js +207 -20
- package/dist/plan/executor.d.ts +134 -2
- package/dist/plan/executor.js +517 -25
- package/dist/plan/hostnames.d.ts +7 -0
- package/dist/plan/hostnames.js +17 -2
- package/dist/plan/locked-hostnames.d.ts +11 -0
- package/dist/plan/locked-hostnames.js +13 -0
- package/dist/plan/mcp.d.ts +57 -0
- package/dist/plan/mcp.js +131 -0
- package/dist/plan/org-names.d.ts +2 -2
- package/dist/plan/org-names.js +16 -1
- package/dist/plan/plan-apply.d.ts +77 -0
- package/dist/plan/plan-apply.js +267 -5
- package/dist/plan/platform-diff.d.ts +9 -0
- package/dist/plan/platform-diff.js +24 -1
- package/dist/plan/platform-providers.d.ts +15 -1
- package/dist/plan/platform-providers.js +90 -5
- package/dist/plan/proxy-labels.d.ts +2 -2
- package/dist/plan/proxy-labels.js +14 -4
- package/dist/plan/release-host.d.ts +59 -0
- package/dist/plan/release-host.js +239 -0
- package/dist/plan/remote-database.d.ts +124 -0
- package/dist/plan/remote-database.js +373 -0
- package/dist/plan/worker-app-database.d.ts +148 -0
- package/dist/plan/worker-app-database.js +424 -0
- package/dist/plan/worker-app.d.ts +320 -0
- package/dist/plan/worker-app.js +792 -0
- package/dist/plan/zitadel-machine-user.d.ts +13 -1
- package/dist/plan/zitadel-machine-user.js +80 -0
- package/dist/platform.d.ts +11 -1
- package/dist/platform.js +4 -1
- package/dist/release/decide.d.ts +95 -0
- package/dist/release/decide.js +242 -0
- package/dist/release/health.d.ts +48 -0
- package/dist/release/health.js +61 -0
- package/dist/release/run.d.ts +40 -0
- package/dist/release/run.js +56 -0
- package/dist/release/state.d.ts +64 -0
- package/dist/release/state.js +126 -0
- package/dist/release/types.d.ts +54 -0
- package/dist/release/types.js +4 -0
- package/dist/requested-changes.d.ts +12 -3
- package/dist/requested-changes.js +22 -0
- package/dist/store/postgres.d.ts +8 -0
- package/dist/store/postgres.js +67 -8
- package/dist/store/schema.d.ts +188 -0
- package/dist/store/schema.js +15 -0
- package/dist/worker-app-config.d.ts +151 -0
- package/dist/worker-app-config.js +438 -0
- package/package.json +1 -1
- package/versions.digests.json +2 -1
- package/versions.json +2 -1
package/dist/plan/hostnames.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ import type { DeclaredOrg, HostingEntry } from '../org.js';
|
|
|
6
6
|
* acted on elsewhere.
|
|
7
7
|
*/
|
|
8
8
|
export declare function entryHostnames(org: DeclaredOrg, entry: HostingEntry): string[];
|
|
9
|
+
/**
|
|
10
|
+
* The one exact name a Worker app is served at (ADR 0002): `<host>.<domain>` or
|
|
11
|
+
* the apex on the org's own domain, else `<slug>-<key>.oikos.host` (one label
|
|
12
|
+
* under `oikos.host`, so the free Universal certificate covers it), and
|
|
13
|
+
* `<slug>.oikos.host` for the entry keyed `app`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function workerAppHostname(org: DeclaredOrg, entry: HostingEntry): string;
|
|
9
16
|
/**
|
|
10
17
|
* The wildcard name (`*.<domain>`) when an entry declares `'*'`, else `null`.
|
|
11
18
|
* Like `entryHostnames`, it is read only while hosting is on, `usesDomain` is
|
package/dist/plan/hostnames.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { APP_ENTRY_KEY } from '../org.js';
|
|
2
|
+
import { workerAppHostnameOf } from '../worker-app-config.js';
|
|
2
3
|
import { agoraHostnameFor } from './agora.js';
|
|
3
4
|
import { bouleHostnameFor } from './boule.js';
|
|
4
5
|
/**
|
|
@@ -9,11 +10,22 @@ import { bouleHostnameFor } from './boule.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export function entryHostnames(org, entry) {
|
|
11
12
|
const { domain } = org;
|
|
13
|
+
if (entry.workerApp)
|
|
14
|
+
return [workerAppHostname(org, entry)];
|
|
12
15
|
if (!org.hosting.usesDomain || !domain) {
|
|
13
16
|
return entry.key === APP_ENTRY_KEY ? [`${org.slug}.oikos.host`] : [];
|
|
14
17
|
}
|
|
15
18
|
return entry.hostnames.flatMap((name) => name === '*' ? [] : name === '@' ? [domain] : [`${name}.${domain}`]);
|
|
16
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* The one exact name a Worker app is served at (ADR 0002): `<host>.<domain>` or
|
|
22
|
+
* the apex on the org's own domain, else `<slug>-<key>.oikos.host` (one label
|
|
23
|
+
* under `oikos.host`, so the free Universal certificate covers it), and
|
|
24
|
+
* `<slug>.oikos.host` for the entry keyed `app`.
|
|
25
|
+
*/
|
|
26
|
+
export function workerAppHostname(org, entry) {
|
|
27
|
+
return workerAppHostnameOf({ slug: org.slug, domain: org.domain, usesDomain: org.hosting.usesDomain }, entry.key, entry.workerApp?.host ?? '@');
|
|
28
|
+
}
|
|
17
29
|
/**
|
|
18
30
|
* The wildcard name (`*.<domain>`) when an entry declares `'*'`, else `null`.
|
|
19
31
|
* Like `entryHostnames`, it is read only while hosting is on, `usesDomain` is
|
|
@@ -98,8 +110,11 @@ export function filesShareHostname(org) {
|
|
|
98
110
|
export function tunnelIngressHostnames(org, loginHost = null) {
|
|
99
111
|
const hostnames = org.site.enabled && org.domain ? [org.domain, `cms.${org.domain}`] : [];
|
|
100
112
|
if (org.hosting.enabled) {
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
// A Worker app is reached through its own Worker route, never the tunnel.
|
|
114
|
+
for (const entry of org.hosting.entries) {
|
|
115
|
+
if (!entry.workerApp)
|
|
116
|
+
hostnames.push(...entryHostnames(org, entry));
|
|
117
|
+
}
|
|
103
118
|
}
|
|
104
119
|
else if (hostnames.length === 0) {
|
|
105
120
|
hostnames.push(legacyAppHostname(org));
|
|
@@ -10,3 +10,14 @@ export declare const OWN_SERVICE_LABELS: readonly ["auth", "mail", "files", "sfu
|
|
|
10
10
|
* oikos.host service names; any other org with no domain holds none.
|
|
11
11
|
*/
|
|
12
12
|
export declare function lockedHostnames(org: DeclaredOrg, platformHostnames: readonly string[]): Set<string>;
|
|
13
|
+
/**
|
|
14
|
+
* What `validateHosting` knows of the org's names: its domain, the names
|
|
15
|
+
* foundry owns, its slug (for the domain-less `<slug>-<key>.oikos.host`) and
|
|
16
|
+
* whether its site is served at the apex.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hostingNames(org: DeclaredOrg, platformHostnames: readonly string[]): {
|
|
19
|
+
domain: string | null;
|
|
20
|
+
locked: Set<string>;
|
|
21
|
+
slug: string;
|
|
22
|
+
siteOwnsApex: boolean;
|
|
23
|
+
};
|
|
@@ -47,3 +47,16 @@ export function lockedHostnames(org, platformHostnames) {
|
|
|
47
47
|
}
|
|
48
48
|
return locked;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* What `validateHosting` knows of the org's names: its domain, the names
|
|
52
|
+
* foundry owns, its slug (for the domain-less `<slug>-<key>.oikos.host`) and
|
|
53
|
+
* whether its site is served at the apex.
|
|
54
|
+
*/
|
|
55
|
+
export function hostingNames(org, platformHostnames) {
|
|
56
|
+
return {
|
|
57
|
+
domain: org.domain,
|
|
58
|
+
locked: lockedHostnames(org, platformHostnames),
|
|
59
|
+
slug: org.slug,
|
|
60
|
+
siteOwnsApex: org.site.enabled && org.site.usesDomain && org.domain !== null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
|
+
import type { DraftStep, StepAction } from './diff.js';
|
|
3
|
+
import type { ZitadelPort } from './executor.js';
|
|
4
|
+
import type { StepExecutionContext } from './plan-apply.js';
|
|
5
|
+
/**
|
|
6
|
+
* Auth ADR 0002, "What foundry declares and applies": the ZITADEL side of
|
|
7
|
+
* `uses.auth.mcp`. Three steps, all absent for an org that declares no `mcp`:
|
|
8
|
+
*
|
|
9
|
+
* - `zitadel:mcp-project` (per entry): an empty project in the customers
|
|
10
|
+
* organisation. It holds no app; it exists so its id can be the token's
|
|
11
|
+
* audience. The id is recorded once, and `entryMcp` (`broker-entries.ts`) is
|
|
12
|
+
* the single reader: the broker entry and the app's `MCP_AUDIENCE` both take
|
|
13
|
+
* it from there.
|
|
14
|
+
* - `zitadel:mcp-access` (per entry): the entry's existing sign-in app issues
|
|
15
|
+
* JWT access tokens and accepts `https://<login host>/mcp-oauth/callback`.
|
|
16
|
+
* - `zitadel:access-token-lifetime` (per org): the instance-wide lifetime goes
|
|
17
|
+
* from 12 hours to one. No app of ours keeps an access token, so no other
|
|
18
|
+
* app is affected (auth ADR 0002, 4). It is set once any org declares `mcp`
|
|
19
|
+
* and is not set back.
|
|
20
|
+
*
|
|
21
|
+
* Blocked, not failing (`CLAUDE.md` rule 2), while the executor's ZITADEL
|
|
22
|
+
* client lacks the calls, or an input is neither recorded nor made by a step in
|
|
23
|
+
* this plan.
|
|
24
|
+
*/
|
|
25
|
+
type Blocked = Array<{
|
|
26
|
+
description: string;
|
|
27
|
+
key: string;
|
|
28
|
+
}>;
|
|
29
|
+
/** A ZITADEL call of the MCP steps that has not answered in this long fails the step; the next apply repeats it. */
|
|
30
|
+
export declare const MCP_CALL_TIMEOUT_MS = 60000;
|
|
31
|
+
/** The want of the access step, recorded when it is done. */
|
|
32
|
+
export declare const mcpAccessWant: (org: DeclaredOrg) => string;
|
|
33
|
+
export declare function planMcp(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], zitadelMcp: boolean): {
|
|
34
|
+
steps: DraftStep[];
|
|
35
|
+
blocked: Blocked;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The empty project in the customers organisation that is an entry's MCP token
|
|
39
|
+
* audience. Found by name first, so a create whose answer was lost is not made
|
|
40
|
+
* twice. Its id is recorded under {@link entryMcpProjectScopeKey}, the one
|
|
41
|
+
* source `entryMcp` reads.
|
|
42
|
+
*/
|
|
43
|
+
export declare function applyMcpProject(zitadel: Pick<ZitadelPort, 'findOrCreateProject'>, org: DeclaredOrg, action: StepAction, context: StepExecutionContext): Promise<{
|
|
44
|
+
resourceId?: string;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* The entry's sign-in app issues JWT access tokens and holds the broker's
|
|
48
|
+
* callback. Records the want last, so a failed call is planned again.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applyMcpAccess(zitadel: Pick<ZitadelPort, 'ensureOidcAppMcpAccess'>, org: DeclaredOrg, action: StepAction, context: StepExecutionContext): Promise<{
|
|
51
|
+
resourceId?: string;
|
|
52
|
+
}>;
|
|
53
|
+
/** The instance access-token lifetime, set to one hour. */
|
|
54
|
+
export declare function applyAccessTokenLifetime(zitadel: Pick<ZitadelPort, 'ensureAccessTokenLifetime'>, context: StepExecutionContext): Promise<{
|
|
55
|
+
resourceId?: string;
|
|
56
|
+
}>;
|
|
57
|
+
export {};
|
package/dist/plan/mcp.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ACCESS_TOKEN_LIFETIME_APPLIED_SCOPE_KEY, MCP_ACCESS_TOKEN_LIFETIME_SECONDS, entryMcpAccessAppliedScopeKey, entryMcpProjectScopeKey, mcpCallbackUri, } from './broker-entries.js';
|
|
2
|
+
import { withCallTimeout } from './worker-app.js';
|
|
3
|
+
/** A ZITADEL call of the MCP steps that has not answered in this long fails the step; the next apply repeats it. */
|
|
4
|
+
export const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
5
|
+
/** The want of the access step, recorded when it is done. */
|
|
6
|
+
export const mcpAccessWant = (org) => JSON.stringify({ accessTokenType: 'jwt', redirectUri: mcpCallbackUri(org) });
|
|
7
|
+
export function planMcp(org, recorded, plannedSteps, zitadelMcp) {
|
|
8
|
+
const steps = [];
|
|
9
|
+
const blocked = [];
|
|
10
|
+
const entries = org.hosting.enabled
|
|
11
|
+
? org.hosting.entries.filter((e) => Boolean(e.uses?.auth?.mcp))
|
|
12
|
+
: [];
|
|
13
|
+
if (entries.length === 0)
|
|
14
|
+
return { steps, blocked };
|
|
15
|
+
const planned = (system, kind, key) => plannedSteps.some((p) => p.action.system === system &&
|
|
16
|
+
p.action.kind === kind &&
|
|
17
|
+
(key === undefined || p.action.key === key));
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const at = `${org.slug}'s ${entry.key} MCP`;
|
|
20
|
+
const stepKey = `${org.id}:${entry.key}`;
|
|
21
|
+
if (!zitadelMcp) {
|
|
22
|
+
blocked.push({
|
|
23
|
+
description: `${at} is blocked, not planned: the host's ZITADEL client cannot set JWT access tokens or the access-token lifetime`,
|
|
24
|
+
key: `zitadel:mcp:${stepKey}`,
|
|
25
|
+
});
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!recorded[entryMcpProjectScopeKey(entry.key)]) {
|
|
29
|
+
if (!recorded[`zitadel:customers:${org.id}`] && !planned('zitadel', 'customers')) {
|
|
30
|
+
blocked.push({
|
|
31
|
+
description: `${at} project is blocked, not planned: the customers organisation id is not recorded yet`,
|
|
32
|
+
key: `zitadel:mcp-project:${stepKey}`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
steps.push({
|
|
37
|
+
action: { system: 'zitadel', kind: 'mcp-project', key: stepKey, desired: {} },
|
|
38
|
+
step: {
|
|
39
|
+
description: `make the empty project that is ${org.slug}'s ${entry.key} MCP token audience`,
|
|
40
|
+
destroys: false,
|
|
41
|
+
migrates: false,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (recorded[entryMcpAccessAppliedScopeKey(entry.key)] !== mcpAccessWant(org)) {
|
|
47
|
+
if (!recorded[`zitadel:app:${entry.key}`] && !planned('zitadel', 'app', entry.key)) {
|
|
48
|
+
blocked.push({
|
|
49
|
+
description: `${at} access is blocked, not planned: the sign-in client is not recorded yet`,
|
|
50
|
+
key: `zitadel:mcp-access:${stepKey}`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
steps.push({
|
|
55
|
+
action: { system: 'zitadel', kind: 'mcp-access', key: stepKey, desired: {} },
|
|
56
|
+
step: {
|
|
57
|
+
description: `${org.slug}'s ${entry.key} sign-in client issues JWT access tokens and accepts ${mcpCallbackUri(org)}`,
|
|
58
|
+
destroys: false,
|
|
59
|
+
migrates: false,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (zitadelMcp &&
|
|
66
|
+
recorded[ACCESS_TOKEN_LIFETIME_APPLIED_SCOPE_KEY] !== String(MCP_ACCESS_TOKEN_LIFETIME_SECONDS)) {
|
|
67
|
+
steps.push({
|
|
68
|
+
action: { system: 'zitadel', kind: 'access-token-lifetime', key: org.id, desired: {} },
|
|
69
|
+
step: {
|
|
70
|
+
description: `set the sign-in instance's access-token lifetime to ${MCP_ACCESS_TOKEN_LIFETIME_SECONDS / 3600} hour (MCP access tokens are short-lived; no app of ours keeps one)`,
|
|
71
|
+
destroys: false,
|
|
72
|
+
migrates: false,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { steps, blocked };
|
|
77
|
+
}
|
|
78
|
+
// ---- the apply -------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* The empty project in the customers organisation that is an entry's MCP token
|
|
81
|
+
* audience. Found by name first, so a create whose answer was lost is not made
|
|
82
|
+
* twice. Its id is recorded under {@link entryMcpProjectScopeKey}, the one
|
|
83
|
+
* source `entryMcp` reads.
|
|
84
|
+
*/
|
|
85
|
+
export async function applyMcpProject(zitadel, org, action, context) {
|
|
86
|
+
const entryKey = action.key.slice(org.id.length + 1);
|
|
87
|
+
const at = `zitadel:mcp-project for ${org.slug} entry '${entryKey}'`;
|
|
88
|
+
if (!org.hosting.entries.some((e) => e.key === entryKey && e.uses?.auth?.mcp)) {
|
|
89
|
+
throw new Error(`${at}: the entry no longer declares uses.auth.mcp`);
|
|
90
|
+
}
|
|
91
|
+
const customersOrgId = context.recordedIds()[`zitadel:customers:${org.id}`];
|
|
92
|
+
if (!customersOrgId)
|
|
93
|
+
throw new Error(`${at}: the customers organisation is not recorded yet`);
|
|
94
|
+
const { project } = await withCallTimeout(MCP_CALL_TIMEOUT_MS, `${at}: find or create the project`, () => zitadel.findOrCreateProject(customersOrgId, `${org.slug}-${entryKey}-mcp`));
|
|
95
|
+
context.recordScopeId(entryMcpProjectScopeKey(entryKey), project.id);
|
|
96
|
+
return { resourceId: project.id };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The entry's sign-in app issues JWT access tokens and holds the broker's
|
|
100
|
+
* callback. Records the want last, so a failed call is planned again.
|
|
101
|
+
*/
|
|
102
|
+
export async function applyMcpAccess(zitadel, org, action, context) {
|
|
103
|
+
const entryKey = action.key.slice(org.id.length + 1);
|
|
104
|
+
const at = `zitadel:mcp-access for ${org.slug} entry '${entryKey}'`;
|
|
105
|
+
const ensure = zitadel.ensureOidcAppMcpAccess?.bind(zitadel);
|
|
106
|
+
if (!ensure)
|
|
107
|
+
throw new Error(`${at}: the ZITADEL client cannot set JWT access tokens`);
|
|
108
|
+
if (!org.hosting.entries.some((e) => e.key === entryKey && e.uses?.auth?.mcp)) {
|
|
109
|
+
throw new Error(`${at}: the entry no longer declares uses.auth.mcp`);
|
|
110
|
+
}
|
|
111
|
+
const recorded = context.recordedIds();
|
|
112
|
+
const zitadelOrgId = recorded.zitadel_org;
|
|
113
|
+
const projectId = recorded.zitadel_project;
|
|
114
|
+
const appId = recorded[`zitadel:app:${entryKey}`];
|
|
115
|
+
if (!zitadelOrgId || !projectId || !appId) {
|
|
116
|
+
throw new Error(`${at}: the sign-in client is not recorded yet`);
|
|
117
|
+
}
|
|
118
|
+
await withCallTimeout(MCP_CALL_TIMEOUT_MS, `${at}: update the app`, () => ensure(zitadelOrgId, projectId, appId, { redirectUris: [mcpCallbackUri(org)] }));
|
|
119
|
+
context.recordScopeId(entryMcpAccessAppliedScopeKey(entryKey), mcpAccessWant(org));
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
/** The instance access-token lifetime, set to one hour. */
|
|
123
|
+
export async function applyAccessTokenLifetime(zitadel, context) {
|
|
124
|
+
const ensure = zitadel.ensureAccessTokenLifetime?.bind(zitadel);
|
|
125
|
+
if (!ensure) {
|
|
126
|
+
throw new Error('zitadel:access-token-lifetime: the ZITADEL client cannot set the lifetime');
|
|
127
|
+
}
|
|
128
|
+
await withCallTimeout(MCP_CALL_TIMEOUT_MS, 'zitadel:access-token-lifetime: set the lifetime', () => ensure(MCP_ACCESS_TOKEN_LIFETIME_SECONDS));
|
|
129
|
+
context.recordScopeId(ACCESS_TOKEN_LIFETIME_APPLIED_SCOPE_KEY, String(MCP_ACCESS_TOKEN_LIFETIME_SECONDS));
|
|
130
|
+
return {};
|
|
131
|
+
}
|
package/dist/plan/org-names.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'cha
|
|
|
12
12
|
* foundry derives for an org: its own-service names when it owns services,
|
|
13
13
|
* and its hosting-entry and database names.
|
|
14
14
|
*/
|
|
15
|
-
export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>): {
|
|
15
|
+
export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>): {
|
|
16
16
|
name: string;
|
|
17
17
|
source: OrgNameSource;
|
|
18
18
|
}[];
|
|
@@ -26,5 +26,5 @@ export interface NameClash {
|
|
|
26
26
|
source: OrgNameSource;
|
|
27
27
|
}
|
|
28
28
|
/** Every name `org` derives that another org or the platform also derives. */
|
|
29
|
-
export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>, others: readonly Pick<DeclaredOrg, 'slug' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>[]): NameClash[];
|
|
29
|
+
export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>[]): NameClash[];
|
|
30
30
|
export declare function nameClashMessage(slug: string, clash: NameClash): string;
|
package/dist/plan/org-names.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ownsServices } from '../org.js';
|
|
2
|
+
import { entryHasDatabase } from '../worker-app-config.js';
|
|
2
3
|
import { chatInternalNames, chatRekeyedInternalNames } from './chat-matrix.js';
|
|
4
|
+
import { workerAppHostname } from './hostnames.js';
|
|
3
5
|
import { orgTarget, platformTarget } from './own-services.js';
|
|
4
6
|
import { appNameFor } from './platform-diff.js';
|
|
5
7
|
/**
|
|
@@ -38,7 +40,17 @@ export function orgDerivedNames(org) {
|
|
|
38
40
|
for (const entry of org.hosting.entries) {
|
|
39
41
|
names.push({ name: appNameFor(org, entry), source: 'hosting' });
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
// A Worker app's public host. The domain-less `<slug>-<key>.oikos.host` of
|
|
44
|
+
// org `foo` key `x` is the `<slug>.oikos.host` of org `foo-x`, so hosts are
|
|
45
|
+
// compared across orgs like every other derived name.
|
|
46
|
+
if (org.hosting.enabled) {
|
|
47
|
+
for (const entry of org.hosting.entries) {
|
|
48
|
+
if (entry.workerApp) {
|
|
49
|
+
names.push({ name: workerAppHostname(org, entry), source: 'hosting' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (org.hosting.entries.some(entryHasDatabase)) {
|
|
42
54
|
names.push({ name: `${org.slug}-db`, source: 'hosting' });
|
|
43
55
|
}
|
|
44
56
|
return names;
|
|
@@ -93,6 +105,9 @@ export function findNameClashes(org, others) {
|
|
|
93
105
|
if (other.slug === org.slug)
|
|
94
106
|
continue;
|
|
95
107
|
const theirs = new Set(orgDerivedNames(other).map((n) => n.name));
|
|
108
|
+
// Org `foo-x` without a domain is served at `foo-x.oikos.host`.
|
|
109
|
+
if (!other.domain)
|
|
110
|
+
theirs.add(`${other.slug}.oikos.host`);
|
|
96
111
|
for (const { name, source } of mine) {
|
|
97
112
|
if (theirs.has(name))
|
|
98
113
|
clashes.push({ name, other: other.slug, source });
|
|
@@ -2,18 +2,21 @@ import type { DestroyProgress, PlanApply } from '../apply.js';
|
|
|
2
2
|
import type { DeclaredOrg, OrgId } from '../org.js';
|
|
3
3
|
import type { Plan, PlanStep } from '../plan.js';
|
|
4
4
|
import type { DeclaredPlatform } from '../platform.js';
|
|
5
|
+
import type { ReleaseStore } from '../release/state.js';
|
|
5
6
|
import type { JobRunner } from '../runner/types.js';
|
|
6
7
|
import type { AppliedState } from '../state.js';
|
|
7
8
|
import { type AppliedVersionsStore, type DeclaredOrgStore, type PlatformStore } from '../store.js';
|
|
8
9
|
import { type AdoptCandidate, type AdoptInput, type AdoptPlan } from './adopt.js';
|
|
9
10
|
import { type AgoraHostPort } from './agora.js';
|
|
10
11
|
import { type MoveGatePorts } from './backup-gate.js';
|
|
12
|
+
import { type BlueGreenServer, type BlueGreenSubject } from './blue-green.js';
|
|
11
13
|
import { type UnconfirmedUpgrade } from './confirm-upgrade.js';
|
|
12
14
|
import { type StepAction } from './diff.js';
|
|
13
15
|
import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
|
|
14
16
|
import { type LoginHostDiscoveryPort } from './login-host.js';
|
|
15
17
|
import { type EdgePortName, type HeldKeyResolver, type PlatformOwnerInput } from './platform-org.js';
|
|
16
18
|
import type { CallsSfuConnectionPort } from './platform-sfu.js';
|
|
19
|
+
import { type ReleaseContext, type ReleaseHost } from './release-host.js';
|
|
17
20
|
/** Who did it, for `@wtfalch/audit`'s "who, when, the plan, the result" (#9, #19). */
|
|
18
21
|
export interface AuditActor {
|
|
19
22
|
readonly id: string;
|
|
@@ -205,6 +208,21 @@ export interface StepExecutor {
|
|
|
205
208
|
}>;
|
|
206
209
|
/** Whether the executor can mint an own org's Lettermint key: a Lettermint port and a reader for the held ref. */
|
|
207
210
|
ownMailKeyMintable?(): boolean;
|
|
211
|
+
/** C3: whether the ZITADEL port can mint and revoke a customers directory credential. */
|
|
212
|
+
customersDirectoryAvailable?(): boolean;
|
|
213
|
+
/** C3: whether the host supplied `mintBouleArchonCredential`. */
|
|
214
|
+
bouleArchonCredentialAvailable?(): boolean;
|
|
215
|
+
/** Every Coolify server with whether it is usable and reachable; blue/green plans copies on each. Optional: an executor without it plans no blue/green. */
|
|
216
|
+
blueGreenServers?(): Promise<Array<{
|
|
217
|
+
uuid: string;
|
|
218
|
+
usable: boolean;
|
|
219
|
+
reachable: boolean;
|
|
220
|
+
}>>;
|
|
221
|
+
/**
|
|
222
|
+
* The release runner's outside calls for one blue/green entry (`plan/release-host.ts`); `null` while its blue
|
|
223
|
+
* copy is not recorded. Optional: an executor without it runs no release.
|
|
224
|
+
*/
|
|
225
|
+
releaseHost?(org: DeclaredOrg, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
|
|
208
226
|
/** Whether the executor's held keys can store the sign-in database's CA and certificate (#746). Absent: true. */
|
|
209
227
|
ownSigninDbKeysPuttable?(): boolean;
|
|
210
228
|
/** Whether the host supplies the backup bucket the sign-in database dumps into (#746). Absent: true. */
|
|
@@ -325,6 +343,16 @@ export interface PlatformStepExecutor {
|
|
|
325
343
|
* one), or `null` when nothing changed.
|
|
326
344
|
*/
|
|
327
345
|
refreshFilesShare?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<Record<string, string> | null>;
|
|
346
|
+
/**
|
|
347
|
+
* The Coolify servers and the reason platform-archon's blue/green cannot run (the executor cannot hold the
|
|
348
|
+
* health token), for the plan. Optional: an executor without it plans no blue/green for archon.
|
|
349
|
+
*/
|
|
350
|
+
archonBlueGreen?(): Promise<{
|
|
351
|
+
servers?: BlueGreenServer[];
|
|
352
|
+
blocked?: string;
|
|
353
|
+
}>;
|
|
354
|
+
/** The release runner's outside calls for platform-archon (`plan/release-host.ts`); `null` while its app is not recorded. */
|
|
355
|
+
releaseHost?(platform: DeclaredPlatform, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
|
|
328
356
|
execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
|
|
329
357
|
resourceId?: string;
|
|
330
358
|
} | undefined>;
|
|
@@ -453,6 +481,12 @@ export interface FoundryPlanApplyOptions {
|
|
|
453
481
|
* token of its existing sign-in apps.
|
|
454
482
|
*/
|
|
455
483
|
zitadelIdTokenUserinfo?: boolean;
|
|
484
|
+
/**
|
|
485
|
+
* Auth ADR 0002: true when the host's ZITADEL client has `ensureOidcAppMcpAccess`
|
|
486
|
+
* and `ensureAccessTokenLifetime` (`@wtfalch/providers` 0.11.0). Without it every
|
|
487
|
+
* `uses.auth.mcp` is reported blocked.
|
|
488
|
+
*/
|
|
489
|
+
zitadelMcp?: boolean;
|
|
456
490
|
/**
|
|
457
491
|
* F5: true when the host's executor was built with a `broker` port.
|
|
458
492
|
* Optional, and false without it: each entry's broker entry is then
|
|
@@ -465,6 +499,17 @@ export interface FoundryPlanApplyOptions {
|
|
|
465
499
|
* reported blocked instead of planned.
|
|
466
500
|
*/
|
|
467
501
|
edgeWorkers?: boolean;
|
|
502
|
+
/**
|
|
503
|
+
* ADR 0002: true when the host's executor was built with a `WorkerAppPort`.
|
|
504
|
+
* Optional, and false without it: each `workerApp` entry is then reported blocked.
|
|
505
|
+
*/
|
|
506
|
+
workerApps?: boolean;
|
|
507
|
+
/** ADR 0002: the keys custody Worker address, when the executor has a `KeysCustodyPort`. A `keysCustody` secret is blocked without it. */
|
|
508
|
+
workerAppKeysUrl?: string;
|
|
509
|
+
/** C2: true when the host's executor was built with a `WorkerAppDatabasePort`. */
|
|
510
|
+
workerAppDatabase?: boolean;
|
|
511
|
+
/** ADR 0002: the platform's Cloudflare account id. A container image outside its registry is blocked. */
|
|
512
|
+
workerAppRegistryAccountId?: string;
|
|
468
513
|
/**
|
|
469
514
|
* True when the host's executor was built with `ownerInviteMail`. Optional,
|
|
470
515
|
* and false without it: each owner's invite is then reported blocked
|
|
@@ -510,6 +555,11 @@ export interface FoundryPlanApplyOptions {
|
|
|
510
555
|
step(action: StepAction): boolean;
|
|
511
556
|
blocked(key: string): boolean;
|
|
512
557
|
};
|
|
558
|
+
/**
|
|
559
|
+
* The release requests (Archon's promote, roll back, clear held and decide-at-cap clicks), consumed once each by
|
|
560
|
+
* the release runner. Without it no release runs, in an apply or in {@link FoundryPlanApply.runReleaseTick}.
|
|
561
|
+
*/
|
|
562
|
+
releaseStore?: ReleaseStore;
|
|
513
563
|
/** Overridable for tests; defaults to `() => new Date()`. */
|
|
514
564
|
now?: () => Date;
|
|
515
565
|
/** #441: how long a mutation lease lasts before another caller may take it over. Defaults to 30 minutes. */
|
|
@@ -590,8 +640,13 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
590
640
|
private readonly loginHostBindings;
|
|
591
641
|
private readonly loginHostDiscovery;
|
|
592
642
|
private readonly zitadelIdTokenUserinfo;
|
|
643
|
+
private readonly zitadelMcp;
|
|
593
644
|
private readonly broker;
|
|
594
645
|
private readonly edgeWorkers;
|
|
646
|
+
private readonly workerApps;
|
|
647
|
+
private readonly workerAppKeysUrl;
|
|
648
|
+
private readonly workerAppDatabase;
|
|
649
|
+
private readonly workerAppRegistryAccountId;
|
|
595
650
|
private readonly ownerInviteMail;
|
|
596
651
|
private readonly hostFiles;
|
|
597
652
|
private readonly hostAi;
|
|
@@ -605,6 +660,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
605
660
|
private readonly backupGate;
|
|
606
661
|
private readonly platformDeps;
|
|
607
662
|
private readonly runner;
|
|
663
|
+
private readonly releaseStore;
|
|
608
664
|
constructor(options: FoundryPlanApplyOptions);
|
|
609
665
|
plan(id: OrgId): Promise<Plan>;
|
|
610
666
|
/**
|
|
@@ -741,6 +797,27 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
741
797
|
private refusePlatform;
|
|
742
798
|
private refuseDestroy;
|
|
743
799
|
private runSteps;
|
|
800
|
+
/**
|
|
801
|
+
* ADR 0001: one release run for each blue/green entry whose structure is applied, writing into `context`
|
|
802
|
+
* (the live ids of the run that holds the lease). A weight the runner changed reaches every server's balancer
|
|
803
|
+
* before this returns.
|
|
804
|
+
*/
|
|
805
|
+
private runOrgReleases;
|
|
806
|
+
/** One release run for platform-archon, on the platform's live ids; see {@link runOrgReleases}. */
|
|
807
|
+
private runPlatformReleases;
|
|
808
|
+
private runPlatformReleaseTick;
|
|
809
|
+
/**
|
|
810
|
+
* The recurring release run (ADR 0001): for every declared org with a blue/green entry, and the platform's
|
|
811
|
+
* archon when it declares one, takes the open requests, reads the health signal and moves traffic. Foundry has
|
|
812
|
+
* no scheduler of its own, so the host calls this every 10 minutes. An org that cannot run (the actor may not
|
|
813
|
+
* apply it, an apply holds its lease, a provider call fails) is reported by id and does not stop the others.
|
|
814
|
+
* A refusal is audited; a run that does nothing writes no audit row.
|
|
815
|
+
*/
|
|
816
|
+
runReleaseTick(): Promise<Array<{
|
|
817
|
+
target: string;
|
|
818
|
+
error?: string;
|
|
819
|
+
}>>;
|
|
820
|
+
private runOrgReleaseTick;
|
|
744
821
|
/** An own org with calls on: the host's answer about its slot's ports. A failure or a slow answer is "not open". */
|
|
745
822
|
private readSfuPortsOpen;
|
|
746
823
|
private computeFreshPlan;
|