@wtfalch/foundry 0.60.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +5 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/org.d.ts +163 -22
- package/dist/org.js +419 -47
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +41 -1
- package/dist/plan/blue-green.js +80 -2
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/desktop-callback.js +12 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +373 -42
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +191 -1
- package/dist/plan/executor.js +1505 -90
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +2 -2
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.js +12 -1
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +7 -2
- package/dist/plan/post-logout-uris.js +7 -1
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +11 -0
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +10 -6
- package/dist/store/postgres.js +12 -2
- package/dist/store/schema.d.ts +37 -1
- package/dist/store/schema.js +4 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +18 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, entryClientIdScopeKey, } from './agora.js';
|
|
2
|
+
import { BOULE_CLIENT_ID_SCOPE_KEY } from './boule.js';
|
|
3
|
+
import { brokerEntryAppliedScopeKey, entryMcpProjectScopeKey, } from './broker-entries.js';
|
|
4
|
+
import { BROKER_WORKSPACES_DATABASE_ENV, workspacesDatabaseRef, workspacesUrl, } from './entry-workspaces.js';
|
|
5
|
+
import { workspaceHost } from './hostnames.js';
|
|
6
|
+
import { loginHostnameFor } from './login-host.js';
|
|
7
|
+
import { appStepKey } from './platform-diff.js';
|
|
8
|
+
import { TEST_CUSTOMERS_SIGN_IN_SCOPE_KEY, TEST_CUSTOMERS_SIGN_UP_SCOPE_KEY, testCustomersEntries, testCustomersOrgKey, workspaceAuthEntries, workspaceClientKey, } from './workspace-client.js';
|
|
9
|
+
/**
|
|
10
|
+
* The sign-in of a server workspace (`HostingEntry.workspace` with `uses.auth`), so the team and invited testers
|
|
11
|
+
* can test the whole app, customer sign-in and customer workspaces included, before it is live. It is built from
|
|
12
|
+
* the same pieces as the live entry's sign-in and shares none of its secrets:
|
|
13
|
+
* - a company with any such entry gets a "<slug> test customers" organisation (`planTestCustomers`), never the
|
|
14
|
+
* live customers one;
|
|
15
|
+
* - the entry gets its own ZITADEL client `<key>-work` at the workspace host (`planWorkspaceClients`);
|
|
16
|
+
* - the stage app gets its own `AUTH_*` values, cookie secret, app key and, with `uses.auth.workspaces`, its own
|
|
17
|
+
* workspaces key;
|
|
18
|
+
* - the broker gets its own entry `<slug>-<key>-work` (`planWorkspaceBrokerEntries`), whose key and workspaces
|
|
19
|
+
* key hash are the stage's, so it has its own product tenant in the broker.
|
|
20
|
+
*/
|
|
21
|
+
const resourceKey = (system, kind, key) => `${system}:${kind}:${key}`;
|
|
22
|
+
/** The step `coolify:workspace-broker-entry` and its action key, for the planner and the executor to share. */
|
|
23
|
+
export const WORKSPACE_BROKER_ENTRY_KIND = 'workspace-broker-entry';
|
|
24
|
+
/**
|
|
25
|
+
* The MCP resource of a workspace with `uses.auth.mcp`: its own endpoint on the workspace host, its own audience
|
|
26
|
+
* project in the test customers organisation (recorded by `zitadel:mcp-project` under the client's name), and the
|
|
27
|
+
* onboarding page on the workspace host. `null` without `mcp` or until the project is recorded.
|
|
28
|
+
*/
|
|
29
|
+
export function workspaceMcp(org, entry, recorded) {
|
|
30
|
+
const mcp = entry.uses?.auth?.mcp;
|
|
31
|
+
const projectId = recorded[entryMcpProjectScopeKey(workspaceClientKey(entry.key))];
|
|
32
|
+
if (!mcp || !projectId)
|
|
33
|
+
return null;
|
|
34
|
+
const host = workspaceHost(org.slug, entry.key);
|
|
35
|
+
return {
|
|
36
|
+
resource: `https://${host}${mcp.path}`,
|
|
37
|
+
projectId,
|
|
38
|
+
loginHost: loginHostnameFor(org),
|
|
39
|
+
...(mcp.onboardingPath ? { onboardingUrl: `https://${host}${mcp.onboardingPath}` } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What the app's env says about who signs in at the workspace, as recorded so far, or `null` until the
|
|
44
|
+
* organisation, client and issuer are all known. Non-secret values only.
|
|
45
|
+
*/
|
|
46
|
+
export function workspaceAuthEnv(org, entry, recorded, issuer) {
|
|
47
|
+
const auth = entry.uses?.auth;
|
|
48
|
+
if (!auth)
|
|
49
|
+
return null;
|
|
50
|
+
const organizationId = auth.users === 'customers' ? recorded[testCustomersOrgKey(org.id)] : recorded.zitadel_org;
|
|
51
|
+
const clientId = recorded[entryClientIdScopeKey(workspaceClientKey(entry.key))];
|
|
52
|
+
if (!organizationId || !clientId || !issuer)
|
|
53
|
+
return null;
|
|
54
|
+
const mcp = workspaceMcp(org, entry, recorded);
|
|
55
|
+
if (auth.mcp && !mcp)
|
|
56
|
+
return null;
|
|
57
|
+
return {
|
|
58
|
+
AUTH_ORGANIZATION_ID: organizationId,
|
|
59
|
+
AUTH_CLIENT_ID: clientId,
|
|
60
|
+
AUTH_ISSUER: issuer,
|
|
61
|
+
...(auth.workspaces ? { WORKSPACES_URL: workspacesUrl(org) } : {}),
|
|
62
|
+
...(mcp
|
|
63
|
+
? {
|
|
64
|
+
MCP_AUDIENCE: mcp.projectId,
|
|
65
|
+
MCP_RESOURCE: mcp.resource,
|
|
66
|
+
MCP_ISSUER: `https://${mcp.loginHost}`,
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const SHARED_CLIENT_KEYS = new Set([
|
|
72
|
+
APP_CLIENT_ID_SCOPE_KEY,
|
|
73
|
+
CMS_CLIENT_ID_SCOPE_KEY,
|
|
74
|
+
AGORA_CLIENT_ID_SCOPE_KEY,
|
|
75
|
+
BOULE_CLIENT_ID_SCOPE_KEY,
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* The client ids of the company's live apps: every recorded client id except the workspaces' own. A stage that
|
|
79
|
+
* holds one of them could sign in as the live app.
|
|
80
|
+
*/
|
|
81
|
+
export function liveClientIds(org, recorded) {
|
|
82
|
+
const own = new Set(workspaceAuthEntries(org).map((e) => entryClientIdScopeKey(workspaceClientKey(e.key))));
|
|
83
|
+
return Object.entries(recorded)
|
|
84
|
+
.filter(([key, id]) => Boolean(id) &&
|
|
85
|
+
(SHARED_CLIENT_KEYS.has(key) ||
|
|
86
|
+
(key.startsWith(ENTRY_CLIENT_ID_SCOPE_PREFIX) && !own.has(key))))
|
|
87
|
+
.map(([, id]) => id)
|
|
88
|
+
.sort();
|
|
89
|
+
}
|
|
90
|
+
/** What `ws.<key>.signin` records: the sign-in env above as JSON, or `null` while it is not all known. */
|
|
91
|
+
export function workspaceSignInMarker(org, entry, recorded, issuer) {
|
|
92
|
+
const env = workspaceAuthEnv(org, entry, recorded, issuer);
|
|
93
|
+
return env ? JSON.stringify(env) : null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The test customers organisation, planned once an entry's workspace signs in customers. Sign-up and the emailed
|
|
97
|
+
* code follow what the company declares for its live customers (`customers.signUp`, the entries' `signIn`), so
|
|
98
|
+
* testers meet the same sign-in the customers will. Idempotent on the recorded id and the two markers.
|
|
99
|
+
*/
|
|
100
|
+
export function planTestCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
101
|
+
const entries = testCustomersEntries(org);
|
|
102
|
+
const wanted = entries.length > 0;
|
|
103
|
+
const signUp = org.customers?.signUp === true;
|
|
104
|
+
const wantedSignIn = entries.some((e) => e.uses?.auth?.signIn === 'email-code')
|
|
105
|
+
? 'email-code'
|
|
106
|
+
: '';
|
|
107
|
+
const blocked = [];
|
|
108
|
+
const orgId = recordedIds[testCustomersOrgKey(org.id)];
|
|
109
|
+
const metadataBlocked = () => ({
|
|
110
|
+
description: `${org.slug}'s test customers organisation sign-in metadata is blocked, not planned: the host's ZITADEL client cannot set or remove organisation metadata`,
|
|
111
|
+
key: `zitadel:customers-test-signin:${org.id}`,
|
|
112
|
+
});
|
|
113
|
+
const draft = (desired, description) => ({
|
|
114
|
+
step: {
|
|
115
|
+
action: { system: 'zitadel', kind: 'customers-test', key: org.id, desired },
|
|
116
|
+
step: { description, destroys: false, migrates: false },
|
|
117
|
+
},
|
|
118
|
+
blocked,
|
|
119
|
+
});
|
|
120
|
+
if (!orgId) {
|
|
121
|
+
if (!wanted)
|
|
122
|
+
return { step: null, blocked };
|
|
123
|
+
const withSignIn = wantedSignIn === 'email-code' && metadataWired;
|
|
124
|
+
if (wantedSignIn === 'email-code' && !metadataWired)
|
|
125
|
+
blocked.push(metadataBlocked());
|
|
126
|
+
return draft({ signUp, ...(withSignIn ? { signIn: wantedSignIn } : {}) }, `provision a ZITADEL org for ${org.slug}'s test customers, with self-registration ${signUp ? 'on' : 'off'}${withSignIn ? ' and sign-in by emailed code' : ''}`);
|
|
127
|
+
}
|
|
128
|
+
for (const r of live) {
|
|
129
|
+
if (r.system === 'zitadel' && r.kind === 'project_grant' && r.config.grantedOrgId === orgId) {
|
|
130
|
+
claimed.add(resourceKey(r.system, r.kind, r.key));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const appliedSignUp = recordedIds[TEST_CUSTOMERS_SIGN_UP_SCOPE_KEY] === '1' ? '1' : '0';
|
|
134
|
+
const wantedSignUp = wanted && signUp ? '1' : '0';
|
|
135
|
+
const appliedSignIn = recordedIds[TEST_CUSTOMERS_SIGN_IN_SCOPE_KEY] === 'email-code' ? 'email-code' : '';
|
|
136
|
+
const signInDiffers = appliedSignIn !== (wanted ? wantedSignIn : '');
|
|
137
|
+
if (signInDiffers && !metadataWired)
|
|
138
|
+
blocked.push(metadataBlocked());
|
|
139
|
+
const writeSignIn = signInDiffers && metadataWired;
|
|
140
|
+
if (appliedSignUp === wantedSignUp && !writeSignIn)
|
|
141
|
+
return { step: null, blocked };
|
|
142
|
+
return draft({
|
|
143
|
+
signUp: wantedSignUp === '1',
|
|
144
|
+
...(writeSignIn ? { signIn: wanted ? wantedSignIn : '' } : {}),
|
|
145
|
+
}, appliedSignUp === wantedSignUp
|
|
146
|
+
? `${wantedSignIn ? 'set' : 'remove'} the emailed-code sign-in metadata of ${org.slug}'s test customers`
|
|
147
|
+
: `turn self-registration ${wantedSignUp === '1' ? 'on' : 'off'} for ${org.slug}'s test customers`);
|
|
148
|
+
}
|
|
149
|
+
/** One ZITADEL web client `<key>-work` per workspace with sign-in, planned once; its redirect addresses name the workspace host. */
|
|
150
|
+
export function planWorkspaceClients(org, byKey, claimed) {
|
|
151
|
+
const steps = [];
|
|
152
|
+
for (const entry of workspaceAuthEntries(org)) {
|
|
153
|
+
const client = workspaceClientKey(entry.key);
|
|
154
|
+
const key = resourceKey('zitadel', 'app', client);
|
|
155
|
+
if (byKey.has(key)) {
|
|
156
|
+
claimed.add(key);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
steps.push({
|
|
160
|
+
action: { system: 'zitadel', kind: 'app', key: client, desired: { appType: 'web' } },
|
|
161
|
+
step: {
|
|
162
|
+
description: `provision the workspace sign-in client '${client}' for ${org.slug}`,
|
|
163
|
+
destroys: false,
|
|
164
|
+
migrates: false,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return steps;
|
|
169
|
+
}
|
|
170
|
+
/** What a workspace broker entry must say, without its keys, or what is still missing. */
|
|
171
|
+
export function wantedWorkspaceBrokerEntry(org, entry, recorded) {
|
|
172
|
+
const auth = entry.uses?.auth;
|
|
173
|
+
if (!auth)
|
|
174
|
+
return { missing: [] };
|
|
175
|
+
const client = workspaceClientKey(entry.key);
|
|
176
|
+
const organizationId = auth.users === 'customers' ? recorded[testCustomersOrgKey(org.id)] : recorded.zitadel_org;
|
|
177
|
+
const clientId = recorded[entryClientIdScopeKey(client)];
|
|
178
|
+
const mailDomain = recorded.mail_domain;
|
|
179
|
+
const missing = [];
|
|
180
|
+
if (!organizationId) {
|
|
181
|
+
missing.push(auth.users === 'customers'
|
|
182
|
+
? { what: 'the test customers organisation id', producedBy: ['zitadel', 'customers-test'] }
|
|
183
|
+
: { what: "the org's ZITADEL org id", producedBy: ['zitadel', 'issuer'] });
|
|
184
|
+
}
|
|
185
|
+
if (!clientId)
|
|
186
|
+
missing.push({ what: 'its workspace sign-in client id', producedBy: ['zitadel', 'app'] });
|
|
187
|
+
if (!mailDomain)
|
|
188
|
+
missing.push({ what: 'the org mail domain', producedBy: ['coolify', 'mail'] });
|
|
189
|
+
const mcp = workspaceMcp(org, entry, recorded);
|
|
190
|
+
if (auth.mcp && !mcp) {
|
|
191
|
+
missing.push({ what: 'its MCP audience project id', producedBy: ['zitadel', 'mcp-project'] });
|
|
192
|
+
}
|
|
193
|
+
if (!organizationId || !clientId || !mailDomain || (auth.mcp && !mcp))
|
|
194
|
+
return { missing };
|
|
195
|
+
const host = workspaceHost(org.slug, entry.key);
|
|
196
|
+
return {
|
|
197
|
+
want: {
|
|
198
|
+
name: `${org.slug}-${client}`,
|
|
199
|
+
organizationId,
|
|
200
|
+
clientId,
|
|
201
|
+
origins: [`https://${host}`],
|
|
202
|
+
from: `${org.mail.noReplyLocalPart || 'noreply'}@${mailDomain}`,
|
|
203
|
+
registration: auth.registration === 'open',
|
|
204
|
+
replaces: null,
|
|
205
|
+
...(mcp ? { mcp } : {}),
|
|
206
|
+
...(auth.workspaces
|
|
207
|
+
? {
|
|
208
|
+
workspaces: {
|
|
209
|
+
roles: [...auth.workspaces.roles],
|
|
210
|
+
inviteUrl: `https://${host}${auth.workspaces.invitePath}`,
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
214
|
+
...(auth.signIn ? { emailLink: auth.signIn === 'email-code' } : {}),
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* One broker `AUTH_APPS` entry per workspace with sign-in, written after the workspace step has put the stage's
|
|
220
|
+
* `AUTH_APP_KEY` in its env. Idempotent on the recorded marker of what the last write said.
|
|
221
|
+
*/
|
|
222
|
+
export function planWorkspaceBrokerEntries(org, recordedIds, plannedSteps, brokerWired, brokerWorkspacesDatabase, stageReady) {
|
|
223
|
+
const steps = [];
|
|
224
|
+
const blocked = [];
|
|
225
|
+
for (const entry of workspaceAuthEntries(org)) {
|
|
226
|
+
const auth = entry.uses?.auth;
|
|
227
|
+
if (!auth)
|
|
228
|
+
continue;
|
|
229
|
+
const at = `${org.slug}'s ${entry.key} workspace broker entry`;
|
|
230
|
+
const key = `coolify:workspace-broker-entry:${entry.key}`;
|
|
231
|
+
const block = (why) => blocked.push({ description: `${at} is blocked, not planned: ${why}`, key });
|
|
232
|
+
if (!brokerWired) {
|
|
233
|
+
block('the host has not wired the broker port');
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (auth.workspaces && !brokerWorkspacesDatabase) {
|
|
237
|
+
block(`the broker has no ${BROKER_WORKSPACES_DATABASE_ENV}; hold the connection string of its workspaces database at ${workspacesDatabaseRef(org)}, then apply again`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!stageReady(entry)) {
|
|
241
|
+
block("the workspace's app has no sign-in key yet");
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const wanted = wantedWorkspaceBrokerEntry(org, entry, recordedIds);
|
|
245
|
+
if ('want' in wanted) {
|
|
246
|
+
if (recordedIds[brokerEntryAppliedScopeKey(workspaceClientKey(entry.key))] ===
|
|
247
|
+
JSON.stringify(wanted.want)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const unplanned = wanted.missing.filter(({ producedBy: [system, kind] }) => !plannedSteps.some((p) => p.action.system === system && p.action.kind === kind));
|
|
253
|
+
if (unplanned.length > 0) {
|
|
254
|
+
block(`${unplanned.map((m) => m.what).join(', ')} not recorded yet`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
steps.push({
|
|
259
|
+
action: {
|
|
260
|
+
system: 'coolify',
|
|
261
|
+
kind: WORKSPACE_BROKER_ENTRY_KIND,
|
|
262
|
+
key: appStepKey(org, entry.key),
|
|
263
|
+
desired: {},
|
|
264
|
+
},
|
|
265
|
+
step: {
|
|
266
|
+
description: `write ${at} beside the others`,
|
|
267
|
+
destroys: false,
|
|
268
|
+
migrates: false,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return { steps, blocked };
|
|
273
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HostingEntry } from '../org.js';
|
|
2
|
+
export declare function registerWorkspaceWorker(copy: HostingEntry, parentKey: string): void;
|
|
3
|
+
/** The key of the entry a workspace Worker copy belongs to, or `null` for a declared entry. */
|
|
4
|
+
export declare function workspaceWorkerParent(entry: HostingEntry): string | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Worker-app workspace is planned as a second Worker app: a copy of the entry under the key `<key>-work`.
|
|
3
|
+
* The copy is never declared; `withWorkspaceWorkers` (`plan/workspace.ts`) makes it and registers it here, so
|
|
4
|
+
* the name, host and zone helpers can tell it from a declared entry. A leaf module: `hostnames.ts` reads it.
|
|
5
|
+
*/
|
|
6
|
+
const PARENT_OF = new WeakMap();
|
|
7
|
+
export function registerWorkspaceWorker(copy, parentKey) {
|
|
8
|
+
PARENT_OF.set(copy, parentKey);
|
|
9
|
+
}
|
|
10
|
+
/** The key of the entry a workspace Worker copy belongs to, or `null` for a declared entry. */
|
|
11
|
+
export function workspaceWorkerParent(entry) {
|
|
12
|
+
return PARENT_OF.get(entry) ?? null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type DeclaredOrg, type HostingEntry } from '../org.js';
|
|
2
|
+
import { type BlueGreenServer, type BlueGreenSubject } from './blue-green.js';
|
|
3
|
+
import type { DraftStep } from './diff.js';
|
|
4
|
+
/** The scope keys of a workspace, beside the stage's `bg.<key>.…` ones. `.` separated, no `:`. */
|
|
5
|
+
export declare const wsKey: {
|
|
6
|
+
/** The hash of the workspace's shape last applied. Also the sign that its route may be written. */
|
|
7
|
+
applied: (key: string) => string;
|
|
8
|
+
/** The 40 character commit the workspace runs; the live app is made from it. */
|
|
9
|
+
commit: (key: string) => string;
|
|
10
|
+
/** The commit the live app was last made from or moved to by a promote (`HostingEntry.liveCommit`). */
|
|
11
|
+
liveCommit: (key: string) => string;
|
|
12
|
+
/** The non-secret sign-in env the workspace was last applied with (JSON); never a key. */
|
|
13
|
+
signIn: (key: string) => string;
|
|
14
|
+
};
|
|
15
|
+
/** The key of a workspace's Access gate in the company. */
|
|
16
|
+
export declare const workspaceGateKey: (entryKey: string) => string;
|
|
17
|
+
/** The key of the gate over the test inbox. Entry keys cannot start with a hyphen, so it never clashes with `ws-<key>`. */
|
|
18
|
+
export declare const INBOX_GATE_KEY = "ws--inbox";
|
|
19
|
+
/** True when the org has a server workspace, and so a stage mail sink to show. */
|
|
20
|
+
export declare const hasInbox: (org: DeclaredOrg) => boolean;
|
|
21
|
+
/** The people-list hash a gate was last made or changed with. */
|
|
22
|
+
export declare const gatePeopleScopeKey: (gateKey: string) => string;
|
|
23
|
+
/** The entries of an org that declare a workspace. */
|
|
24
|
+
export declare function workspaceEntries(org: DeclaredOrg): HostingEntry[];
|
|
25
|
+
/** True for a workspace that runs as a server (Coolify), false for a Worker app's. */
|
|
26
|
+
export declare const isServerWorkspace: (entry: HostingEntry) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The company's people who may open company apps: the owners, by sign-in address and work address. This is the
|
|
29
|
+
* list the stage gate used (owners only); the owners are the only company people the gate inputs hold.
|
|
30
|
+
*/
|
|
31
|
+
export declare function companyPeople(org: Pick<DeclaredOrg, 'owners'>): string[];
|
|
32
|
+
/** Who passes a workspace's gate: the company's people and the entry's testers, lowercased, distinct, sorted. */
|
|
33
|
+
export declare function workspacePeople(org: Pick<DeclaredOrg, 'owners'>, entry: HostingEntry): string[];
|
|
34
|
+
/** The hash recorded when a gate's people were set. A change of testers changes it. */
|
|
35
|
+
export declare const peopleHash: (emails: readonly string[]) => string;
|
|
36
|
+
/** The org without the entries that are not live: nothing that makes an app or its address sees them. */
|
|
37
|
+
export declare function withoutDarkEntries(org: DeclaredOrg): DeclaredOrg;
|
|
38
|
+
/**
|
|
39
|
+
* The org with a second Worker app per Worker-app workspace, keyed `<key>-work`, so the Worker app planner and
|
|
40
|
+
* executor deploy it like any other. The copy has no database and no sign-in env (the workspace gate is its
|
|
41
|
+
* sign-in; a database would be the live one) and is served at the workspace host. `declared` names the org the
|
|
42
|
+
* workspaces are read from when `org` has had dark entries removed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function withWorkspaceWorkers(org: DeclaredOrg, declared?: DeclaredOrg): DeclaredOrg;
|
|
45
|
+
/** A server workspace as the blue/green stage code sees its subject: no colors, no hostnames, no reporting. */
|
|
46
|
+
export declare function workspaceSubject(org: DeclaredOrg, entry: HostingEntry): BlueGreenSubject;
|
|
47
|
+
/** The origin the tunnel sends a server workspace's host to: the stage app on the company network. */
|
|
48
|
+
export declare const workspaceOrigin: (subject: Pick<BlueGreenSubject, "base" | "port">) => string;
|
|
49
|
+
/**
|
|
50
|
+
* The tunnel routes of the server workspaces: the workspace host to the stage app. A route is written only once
|
|
51
|
+
* the workspace was applied and its stage guards hold (isolated, probed, gated, healthy), never before.
|
|
52
|
+
*/
|
|
53
|
+
export declare function workspaceRoutes(org: DeclaredOrg, recorded?: Readonly<Record<string, string>>): Array<{
|
|
54
|
+
hostname: string;
|
|
55
|
+
service: string;
|
|
56
|
+
}>;
|
|
57
|
+
/** Whether `name` is the workspace host of one of the org's server workspaces. */
|
|
58
|
+
export declare function ownsWorkspaceName(org: DeclaredOrg, name: string): boolean;
|
|
59
|
+
/** A short hash of what decides the shape of a server workspace, so a change replans it. */
|
|
60
|
+
export declare function workspaceStructureHash(org: DeclaredOrg, entry: HostingEntry): string;
|
|
61
|
+
export interface PlanWorkspacesInput {
|
|
62
|
+
/** The org as declared, dark entries included. */
|
|
63
|
+
org: DeclaredOrg;
|
|
64
|
+
recordedIds: Readonly<Record<string, string>>;
|
|
65
|
+
/** Every Coolify server the host reports. `undefined`: the host cannot list them. */
|
|
66
|
+
servers: readonly BlueGreenServer[] | undefined;
|
|
67
|
+
/** The workspace's Access gate is recorded or planned in this run. */
|
|
68
|
+
gateReady: (gateKey: string) => boolean;
|
|
69
|
+
/** The company's stage mail sink is recorded and its live app is stopped or gone. */
|
|
70
|
+
stageSinkDown?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The sign-in env a workspace with `uses.auth` should run with, as `workspaceSignInMarker` writes it, or
|
|
73
|
+
* `null` while it is not all recorded (the workspace is then blocked). Absent: sign-in is not planned.
|
|
74
|
+
*/
|
|
75
|
+
signInWanted?: (entry: HostingEntry) => string | null;
|
|
76
|
+
/** The issuer the workspace's app signs in at; goes to the step for the executor. */
|
|
77
|
+
authIssuer?: string | null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One `coolify:workspace` step per server workspace whose shape is not applied yet or whose stage needs another
|
|
81
|
+
* try; the Worker-app workspaces are planned by `planWorkerApps` through {@link withWorkspaceWorkers}. Pure.
|
|
82
|
+
* Blocked, not planned, while the host cannot list servers, the primary is unusable, the stage would not be
|
|
83
|
+
* isolated from the company's other apps, or the gate is not there: a workspace nobody can sign in to is not made.
|
|
84
|
+
*/
|
|
85
|
+
export declare function planWorkspaces(input: PlanWorkspacesInput): {
|
|
86
|
+
steps: DraftStep[];
|
|
87
|
+
unmanaged: Array<{
|
|
88
|
+
description: string;
|
|
89
|
+
key: string;
|
|
90
|
+
}>;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The commit of the newest finished deployment of the workspace's stage app, or `null` when the port cannot
|
|
94
|
+
* list deployments, the read fails or there is none. The live app of a workspace entry is made from it.
|
|
95
|
+
*/
|
|
96
|
+
export declare function readWorkspaceCommit(coolify: {
|
|
97
|
+
listApplicationDeployments?(uuid: string, take?: number): Promise<Array<{
|
|
98
|
+
commit?: string;
|
|
99
|
+
status: string;
|
|
100
|
+
}>>;
|
|
101
|
+
}, stageAppId: string): Promise<string | null>;
|