@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,91 @@
|
|
|
1
|
+
import { withTimeout } from './call-timeout.js';
|
|
2
|
+
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
3
|
+
import { recordOwnDnsHostname } from './own-dns.js';
|
|
4
|
+
/**
|
|
5
|
+
* A company's jobs scheduler on a public hostname: a tunnel that dials the scheduler by its internal address,
|
|
6
|
+
* a connector app, and a proxied CNAME for `jobs.<domain>`. An entry runs on its own company's network and
|
|
7
|
+
* cannot dial the scheduler's container name, so `uses.jobs` gives it `https://<hostname>` once this is
|
|
8
|
+
* recorded. The owner company (hq) runs the scheduler every shared-services company registers on; a company
|
|
9
|
+
* that runs its own services publishes its own the same way. No Coolify domain and no Access app: the
|
|
10
|
+
* scheduler authenticates every caller itself (a project's sync key, the admin token), and a gate in front
|
|
11
|
+
* would stop the apps.
|
|
12
|
+
*
|
|
13
|
+
* One `coolify:own-jobs-route` step. It records {@link JOBS_PUBLIC_URL_SCOPE_KEY} last, so a run that fails
|
|
14
|
+
* halfway is planned again, finds the tunnel and connector by name, and no entry is given a dead URL.
|
|
15
|
+
*/
|
|
16
|
+
export const JOBS_PUBLIC_URL_SCOPE_KEY = 'jobs_public_url';
|
|
17
|
+
export const OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY = 'cloudflare_own_jobs_tunnel';
|
|
18
|
+
export const OWN_JOBS_ROUTE_CONNECTOR_APP_SCOPE_KEY = 'coolify_own_jobs_connector_app';
|
|
19
|
+
/**
|
|
20
|
+
* The only paths the public route serves: what a project's app calls (`/sync`, `/timers`, `/timers/<name>`,
|
|
21
|
+
* `/deliveries`). `/admin/*` is not on the internet; foundry reaches it on the Coolify network.
|
|
22
|
+
*/
|
|
23
|
+
export const JOBS_ENTRY_PATH_PATTERN = '^/(sync$|timers(/|$)|deliveries$)';
|
|
24
|
+
export const JOBS_ROUTE_CALL_TIMEOUT_MS = 60_000;
|
|
25
|
+
/** The tunnel's name and the connector app's, one pair per company. */
|
|
26
|
+
export const jobsRouteNames = (target) => ({
|
|
27
|
+
tunnel: `${target.id}-jobs-tunnel`,
|
|
28
|
+
connector: `${target.id}-jobs-cloudflared`,
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Makes the route in this order: tunnel, its ingress, the connector app, the CNAME, then the recorded URL.
|
|
32
|
+
* Each is found by name or content first, so a second run makes nothing twice. A failure leaves what was made
|
|
33
|
+
* (found again on the next run) and no recorded URL. Errors carry the call's label only.
|
|
34
|
+
*/
|
|
35
|
+
export async function provisionJobsRoute(scope, ports, target, input) {
|
|
36
|
+
const ms = ports.callTimeoutMs ?? JOBS_ROUTE_CALL_TIMEOUT_MS;
|
|
37
|
+
const at = `own step coolify:own-jobs-route for ${target.id}`;
|
|
38
|
+
const names = jobsRouteNames(target);
|
|
39
|
+
const record = async (key, id) => {
|
|
40
|
+
scope.recordScopeId(key, id);
|
|
41
|
+
await scope.persist();
|
|
42
|
+
};
|
|
43
|
+
const zoneId = await withTimeout(`${at}: zone lookup`, ms, () => ports.network.zoneId(input.apex));
|
|
44
|
+
if (!zoneId)
|
|
45
|
+
throw new Error(`${at}: no Cloudflare zone for ${input.apex}`);
|
|
46
|
+
const tunnels = await ports.network.tunnels();
|
|
47
|
+
const dns = await ports.network.dns();
|
|
48
|
+
const { environment } = await withTimeout(`${at}: environment`, ms, () => ports.coolify.findOrCreateEnvironment(input.projectUuid, 'production'));
|
|
49
|
+
const tunnel = (await withTimeout(`${at}: tunnel lookup`, ms, () => tunnels.findTunnelByName(names.tunnel))) ??
|
|
50
|
+
(await withTimeout(`${at}: tunnel create`, ms, () => tunnels.createTunnel(names.tunnel)));
|
|
51
|
+
await record(OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY, tunnel.id);
|
|
52
|
+
await withTimeout(`${at}: tunnel ingress`, ms, () => tunnels.putTunnelConfig(tunnel.id, {
|
|
53
|
+
ingress: [
|
|
54
|
+
{ hostname: input.hostname, path: JOBS_ENTRY_PATH_PATTERN, service: input.internalUrl },
|
|
55
|
+
{ service: 'http_status:404' },
|
|
56
|
+
],
|
|
57
|
+
}));
|
|
58
|
+
await withTimeout(`${at}: connector app`, ms * 5, () => ensureCloudflaredConnector({
|
|
59
|
+
coolify: ports.coolify,
|
|
60
|
+
tunnels,
|
|
61
|
+
tunnelId: tunnel.id,
|
|
62
|
+
appName: names.connector,
|
|
63
|
+
projectUuid: input.projectUuid,
|
|
64
|
+
environment,
|
|
65
|
+
resolveServer: async () => {
|
|
66
|
+
const servers = await ports.coolify.listServers();
|
|
67
|
+
const server = servers.find((s) => s.is_usable && s.is_reachable) ?? servers[0];
|
|
68
|
+
if (!server)
|
|
69
|
+
throw new Error('no usable Coolify server is registered');
|
|
70
|
+
return server;
|
|
71
|
+
},
|
|
72
|
+
recordedConnectorAppId: scope.recordedIds()[OWN_JOBS_ROUTE_CONNECTOR_APP_SCOPE_KEY] ?? null,
|
|
73
|
+
recordAppId: (appId) => record(OWN_JOBS_ROUTE_CONNECTOR_APP_SCOPE_KEY, appId),
|
|
74
|
+
}));
|
|
75
|
+
const wanted = {
|
|
76
|
+
type: 'CNAME',
|
|
77
|
+
name: input.hostname,
|
|
78
|
+
content: `${tunnel.id}.cfargotunnel.com`,
|
|
79
|
+
proxied: true,
|
|
80
|
+
};
|
|
81
|
+
const [live] = await withTimeout(`${at}: dns lookup`, ms, () => dns.findDnsRecord(input.apex, input.hostname, 'CNAME'));
|
|
82
|
+
if (!live) {
|
|
83
|
+
await withTimeout(`${at}: dns create`, ms, () => dns.createDnsRecord(input.apex, wanted));
|
|
84
|
+
}
|
|
85
|
+
else if (live.content !== wanted.content) {
|
|
86
|
+
await withTimeout(`${at}: dns update`, ms, () => dns.updateDnsRecord(input.apex, live.id, wanted));
|
|
87
|
+
}
|
|
88
|
+
await recordOwnDnsHostname(scope, input.hostname);
|
|
89
|
+
await record(JOBS_PUBLIC_URL_SCOPE_KEY, `https://${input.hostname}`);
|
|
90
|
+
return { resourceId: tunnel.id };
|
|
91
|
+
}
|
|
@@ -27,6 +27,8 @@ export interface ServiceTarget {
|
|
|
27
27
|
authZitadel: string;
|
|
28
28
|
authLogin: string;
|
|
29
29
|
authBroker: string;
|
|
30
|
+
/** The Coolify Postgres database of the broker's customer workspaces. */
|
|
31
|
+
workspacesDb: string;
|
|
30
32
|
authTunnel: string;
|
|
31
33
|
authCloudflared: string;
|
|
32
34
|
mailApp: string;
|
|
@@ -61,6 +63,8 @@ export interface ServiceTarget {
|
|
|
61
63
|
instanceBearer: string;
|
|
62
64
|
authAdmin: string;
|
|
63
65
|
brokerMailKey: string;
|
|
66
|
+
/** The connection string of the database for the broker's customer workspaces: foundry holds it after making the database, or the operator holds one first. */
|
|
67
|
+
brokerWorkspacesDatabase: string;
|
|
64
68
|
mailAdminKey: string;
|
|
65
69
|
filesManagement: string;
|
|
66
70
|
filesIntrospection: string;
|
|
@@ -16,6 +16,7 @@ export function platformTarget() {
|
|
|
16
16
|
authZitadel: 'platform-auth-zitadel',
|
|
17
17
|
authLogin: 'platform-auth-login',
|
|
18
18
|
authBroker: 'platform-auth-broker',
|
|
19
|
+
workspacesDb: 'platform-workspaces-db',
|
|
19
20
|
authTunnel: 'platform-auth-tunnel',
|
|
20
21
|
authCloudflared: 'platform-auth-cloudflared',
|
|
21
22
|
mailApp: 'platform-mail-stalwart',
|
|
@@ -47,6 +48,7 @@ export function platformTarget() {
|
|
|
47
48
|
instanceBearer: 'zitadel:platform:instanceBearer',
|
|
48
49
|
authAdmin: 'zitadel:platform:admin',
|
|
49
50
|
brokerMailKey: 'lettermint:platform:apiToken',
|
|
51
|
+
brokerWorkspacesDatabase: 'auth:platform:workspacesDatabaseUrl',
|
|
50
52
|
mailAdminKey: 'stalwart:platform:admin',
|
|
51
53
|
filesManagement: 'files:platform:managementKey',
|
|
52
54
|
filesIntrospection: 'zitadel:platform:files:introspection',
|
|
@@ -71,6 +73,7 @@ export function orgTarget(org) {
|
|
|
71
73
|
authZitadel: `${s}-auth-zitadel`,
|
|
72
74
|
authLogin: `${s}-auth-login`,
|
|
73
75
|
authBroker: `${s}-auth-broker`,
|
|
76
|
+
workspacesDb: `${s}-workspaces-db`,
|
|
74
77
|
authTunnel: `${s}-auth-tunnel`,
|
|
75
78
|
authCloudflared: `${s}-auth-cloudflared`,
|
|
76
79
|
mailApp: `${s}-mail-stalwart`,
|
|
@@ -102,6 +105,7 @@ export function orgTarget(org) {
|
|
|
102
105
|
instanceBearer: `zitadel:org:${s}:instanceBearer`,
|
|
103
106
|
authAdmin: `zitadel:org:${s}:admin`,
|
|
104
107
|
brokerMailKey: `lettermint:org:${s}:apiToken`,
|
|
108
|
+
brokerWorkspacesDatabase: `auth:org:${s}:workspacesDatabaseUrl`,
|
|
105
109
|
mailAdminKey: `stalwart:org:${s}:admin`,
|
|
106
110
|
filesManagement: `files:org:${s}:managementKey`,
|
|
107
111
|
filesIntrospection: `zitadel:org:${s}:files:introspection`,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DestroyProgress, PlanApply } from '../apply.js';
|
|
2
|
+
import type { SealResult } from '../audit/seal.js';
|
|
3
|
+
import { type AuditSealingOptions } from '../audit/tick.js';
|
|
2
4
|
import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
|
|
3
5
|
import type { Plan, PlanStep } from '../plan.js';
|
|
4
6
|
import type { DeclaredPlatform } from '../platform.js';
|
|
@@ -12,6 +14,7 @@ import { type MoveGatePorts } from './backup-gate.js';
|
|
|
12
14
|
import { type BlueGreenServer, type BlueGreenSubject } from './blue-green.js';
|
|
13
15
|
import { type UnconfirmedUpgrade } from './confirm-upgrade.js';
|
|
14
16
|
import { type StepAction } from './diff.js';
|
|
17
|
+
import type { GithubDeployKeysPort } from './github-deploy-keys.js';
|
|
15
18
|
import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
|
|
16
19
|
import { type LoginHostDiscoveryPort } from './login-host.js';
|
|
17
20
|
import { type OrgPostgresMigrationConfirmation } from './org-postgres-migrate.js';
|
|
@@ -249,6 +252,14 @@ export interface StepExecutor {
|
|
|
249
252
|
* without it leaves the option as the answer.
|
|
250
253
|
*/
|
|
251
254
|
ownBrokerPort?(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
|
|
255
|
+
/**
|
|
256
|
+
* Whether the host can make the broker's workspaces database: it can hold the URL and the broker and the held keys
|
|
257
|
+
* were just read and have none. Read only for an org whose entry declares workspaces and only while
|
|
258
|
+
* {@link brokerHasWorkspacesDatabase} is false. Optional: without it the plan only reports the entry blocked.
|
|
259
|
+
*/
|
|
260
|
+
canProvisionWorkspacesDatabase?(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
|
|
261
|
+
/** Whether the broker this org's entries are written to has `WORKSPACES_DATABASE_URL`. Read only for an org whose entry declares workspaces. */
|
|
262
|
+
brokerHasWorkspacesDatabase?(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
|
|
252
263
|
/**
|
|
253
264
|
* Every secret this executor minted or read so far (peer password, bridge
|
|
254
265
|
* secret, invite code and link). The host's run adds them to its redactor
|
|
@@ -462,6 +473,8 @@ export interface FoundryApplyOptions {
|
|
|
462
473
|
typed: string;
|
|
463
474
|
/** The removed entry's gate keys, read by the caller from the company before the change. */
|
|
464
475
|
gates?: readonly string[];
|
|
476
|
+
/** The paths the entry's private pages had before the change, for an entry that still exists: their `pp-`/`wp-` gate deletes pass with its confirmation. */
|
|
477
|
+
privatePages?: readonly string[];
|
|
465
478
|
};
|
|
466
479
|
}
|
|
467
480
|
export interface FoundryPlanApplyOptions {
|
|
@@ -550,6 +563,8 @@ export interface FoundryPlanApplyOptions {
|
|
|
550
563
|
workerApps?: boolean;
|
|
551
564
|
/** ADR 0002: the keys custody Worker address, when the executor has a `KeysCustodyPort`. A `keysCustody` secret is blocked without it. */
|
|
552
565
|
workerAppKeysUrl?: string;
|
|
566
|
+
/** F7: the keys service's registrar address, when the executor has a `KeysRegistrarPort`. */
|
|
567
|
+
keysRegistrarUrl?: string;
|
|
553
568
|
/** C2: true when the host's executor was built with a `WorkerAppDatabasePort`. */
|
|
554
569
|
workerAppDatabase?: boolean;
|
|
555
570
|
/** ADR 0002: the platform's Cloudflare account id. A container image outside its registry is blocked. */
|
|
@@ -604,6 +619,16 @@ export interface FoundryPlanApplyOptions {
|
|
|
604
619
|
* the release runner. Without it no release runs, in an apply or in {@link FoundryPlanApply.runReleaseTick}.
|
|
605
620
|
*/
|
|
606
621
|
releaseStore?: ReleaseStore;
|
|
622
|
+
/**
|
|
623
|
+
* F12: the GitHub deploy-key client (`createGithubDeployKeys`), to ask which deploy keys an entry's repo has.
|
|
624
|
+
* Without it an entry with `uses.repo` is reported blocked and nothing else changes.
|
|
625
|
+
*/
|
|
626
|
+
githubDeployKeys?: GithubDeployKeysPort;
|
|
627
|
+
/**
|
|
628
|
+
* `uses.audit` (foundry#929): what {@link FoundryPlanApply.runAuditSealTick} seals with. Without it the tick
|
|
629
|
+
* seals nothing and logs that it was not given.
|
|
630
|
+
*/
|
|
631
|
+
auditSealing?: AuditSealingOptions;
|
|
607
632
|
/** Overridable for tests; defaults to `() => new Date()`. */
|
|
608
633
|
now?: () => Date;
|
|
609
634
|
/** #441: how long a mutation lease lasts before another caller may take it over. Defaults to 30 minutes. */
|
|
@@ -693,6 +718,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
693
718
|
private readonly edgeWorkers;
|
|
694
719
|
private readonly workerApps;
|
|
695
720
|
private readonly workerAppKeysUrl;
|
|
721
|
+
private readonly keysRegistrarUrl;
|
|
696
722
|
private readonly workerAppDatabase;
|
|
697
723
|
private readonly workerAppRegistryAccountId;
|
|
698
724
|
private readonly ownerInviteMail;
|
|
@@ -709,6 +735,8 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
709
735
|
private readonly platformDeps;
|
|
710
736
|
private readonly runner;
|
|
711
737
|
private readonly releaseStore;
|
|
738
|
+
private readonly githubDeployKeys;
|
|
739
|
+
private readonly auditSealing;
|
|
712
740
|
constructor(options: FoundryPlanApplyOptions);
|
|
713
741
|
plan(id: OrgId): Promise<Plan>;
|
|
714
742
|
/**
|
|
@@ -872,6 +900,13 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
872
900
|
target: string;
|
|
873
901
|
error?: string;
|
|
874
902
|
}>>;
|
|
903
|
+
/**
|
|
904
|
+
* The recurring audit sealing run (foundry#929): seals every ledger registered by an `audit:ledger` step with the
|
|
905
|
+
* platform signer and has the timestamp authority stamp the new checkpoints. Foundry has no scheduler, so the host
|
|
906
|
+
* calls this every `AUDIT_SEAL_EVERY_MINUTES` (15). One ledger failing never stops the others; each result and a
|
|
907
|
+
* log line say what happened and why when nothing did. It writes nothing to the org's records.
|
|
908
|
+
*/
|
|
909
|
+
runAuditSealTick(): Promise<SealResult[]>;
|
|
875
910
|
private runOrgReleaseTick;
|
|
876
911
|
/** An own org with calls on: the host's answer about its slot's ports. A failure or a slow answer is "not open". */
|
|
877
912
|
private readSfuPortsOpen;
|
|
@@ -891,6 +926,10 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
891
926
|
* org step that needs one is blocked.
|
|
892
927
|
*/
|
|
893
928
|
private orgPlatformReadiness;
|
|
929
|
+
/** F12: asks GitHub about the repo of each entry with a deploy key to make, check or remove. Never throws; a failure is a plain line. */
|
|
930
|
+
private repoKeysReadiness;
|
|
931
|
+
/** Which refs of the platform GitHub App hold no value now. A resolver that throws counts as not held; only refs are kept. */
|
|
932
|
+
private githubAppKeysReadiness;
|
|
894
933
|
/**
|
|
895
934
|
* The org's `heldSecret` envs: the refs that hold a value now, which of those differ from the live
|
|
896
935
|
* production env, which could not be read, and which fit no ref. A resolver that throws counts as
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { runAuditSealTick } from '../audit/tick.js';
|
|
2
3
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
3
4
|
import { sfuSlotPorts } from '../calls-sfu.js';
|
|
4
5
|
import { ORG_SERVICE_SCHEMAS, heldEnvRef, ownsServices, validateHosting } from '../org.js';
|
|
@@ -8,6 +9,7 @@ import { StaleLeaseError, } from '../store.js';
|
|
|
8
9
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
9
10
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
10
11
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
12
|
+
import { APPS_DB_ENTRY_KEY } from './apps-db-route.js';
|
|
11
13
|
import { NetworkMoveBackupMissingError, OrgPostgresMigrationBackupMissingError, SigninDbMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupForOrgPostgresMigration, requireFreshBackupForSigninMove, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
12
14
|
import { ARCHON_REPO, PRIMARY_SERVER_SCOPE_KEY, archonSubject, blueGreenSubjects, declaresBlueGreen, lbOrigin, } from './blue-green.js';
|
|
13
15
|
import { readLiveBuilds } from './build-record.js';
|
|
@@ -15,22 +17,27 @@ import { LEGACY_BACKUP_NAMES } from './company-backups.js';
|
|
|
15
17
|
import { unconfirmedUpgrades } from './confirm-upgrade.js';
|
|
16
18
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
17
19
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
18
|
-
import { BOULE_EDGE_ENTRY_KEY, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
20
|
+
import { BOULE_EDGE_ENTRY_KEY, bouleEdgeKeyWanted, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
19
21
|
import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
|
|
22
|
+
import { probeRepoKeys, repoKeyProbeTargets, repoKeysInPlay, } from './entry-repo-key.js';
|
|
23
|
+
import { PLATFORM_GITHUB_APP_REFS } from './held-key-refs.js';
|
|
24
|
+
import { KEYS_REGISTRAR_MINT_STARTED_SCOPE_KEY, KEYS_REGISTRAR_SCOPE_KEY, KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY, } from './keys-registrar.js';
|
|
20
25
|
import { readLivePlatformResources, readLiveResources, } from './live.js';
|
|
21
26
|
import { hostingNames } from './locked-hostnames.js';
|
|
22
27
|
import { agoraIssuerHostFor, loginHostServesAgora, readLoginHostDiscovery, } from './login-host.js';
|
|
23
28
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMTP_PORT_MAX, OWN_MAIL_SMTP_PORT_SCOPE_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, isOwnMailSmtpPort, mailRelayFingerprint, } from './mail-relay.js';
|
|
29
|
+
import { ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY } from './org-apps-admin.js';
|
|
24
30
|
import { findNameClashes, nameClashMessage } from './org-names.js';
|
|
25
31
|
import { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationNotConfirmedError, invalidMigrationConfirmation, migrateHomeOf, parseMigrationConfirmation, } from './org-postgres-migrate.js';
|
|
26
32
|
import { ownHostnames } from './own-hostnames.js';
|
|
27
33
|
import { OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, orgTarget } from './own-services.js';
|
|
28
34
|
import { PLATFORM_ARCHON_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
29
|
-
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, dnsBlockedReason, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, ownerChatFrom, ownerFilesFrom, ownerSignInFrom, platformOwnerUserIdFrom, serviceGitRefsOf, } from './platform-org.js';
|
|
35
|
+
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, dnsBlockedReason, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, ownerChatFrom, ownerFilesFrom, ownerJobsPublicUrl, ownerSignInFrom, platformOwnerUserIdFrom, serviceGitRefsOf, } from './platform-org.js';
|
|
30
36
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY, } from './platform-providers.js';
|
|
31
|
-
import {
|
|
37
|
+
import { privatePageGateKey } from './private-pages.js';
|
|
38
|
+
import { CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY, CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, } from './proxy-labels.js';
|
|
32
39
|
import { runSubjectRelease } from './release-host.js';
|
|
33
|
-
import { remoteDatabaseFactsFrom } from './remote-database.js';
|
|
40
|
+
import { remoteDatabaseFactsFrom, remoteDatabaseKeys } from './remote-database.js';
|
|
34
41
|
import { liveWorkerApps, workerAppTraceScopeKeys } from './worker-app.js';
|
|
35
42
|
import { liveWorkers, workerTraceScopeKeys } from './worker.js';
|
|
36
43
|
/** #441: the lease scope that serializes every mutation of one company, and the one for the shared platform. */
|
|
@@ -129,7 +136,10 @@ const HOSTNAME_STEPS = new Set([
|
|
|
129
136
|
'coolify:bluegreen',
|
|
130
137
|
'coolify:bluegreen-lb',
|
|
131
138
|
'coolify:bluegreen-connector',
|
|
139
|
+
'coolify:workspace',
|
|
140
|
+
'coolify:promote',
|
|
132
141
|
'coolify:broker-entry',
|
|
142
|
+
'coolify:workspace-broker-entry',
|
|
133
143
|
'coolify:database',
|
|
134
144
|
'cloudflare:tunnel',
|
|
135
145
|
'cloudflare:access',
|
|
@@ -173,6 +183,7 @@ export class FoundryPlanApply {
|
|
|
173
183
|
edgeWorkers;
|
|
174
184
|
workerApps;
|
|
175
185
|
workerAppKeysUrl;
|
|
186
|
+
keysRegistrarUrl;
|
|
176
187
|
workerAppDatabase;
|
|
177
188
|
workerAppRegistryAccountId;
|
|
178
189
|
ownerInviteMail;
|
|
@@ -189,7 +200,11 @@ export class FoundryPlanApply {
|
|
|
189
200
|
platformDeps;
|
|
190
201
|
runner;
|
|
191
202
|
releaseStore;
|
|
203
|
+
githubDeployKeys;
|
|
204
|
+
auditSealing;
|
|
192
205
|
constructor(options) {
|
|
206
|
+
this.githubDeployKeys = options.githubDeployKeys;
|
|
207
|
+
this.auditSealing = options.auditSealing;
|
|
193
208
|
this.store = options.store;
|
|
194
209
|
this.readers = options.readers;
|
|
195
210
|
this.executor = options.executor;
|
|
@@ -211,6 +226,7 @@ export class FoundryPlanApply {
|
|
|
211
226
|
this.edgeWorkers = options.edgeWorkers ?? false;
|
|
212
227
|
this.workerApps = options.workerApps ?? false;
|
|
213
228
|
this.workerAppKeysUrl = options.workerAppKeysUrl;
|
|
229
|
+
this.keysRegistrarUrl = options.keysRegistrarUrl;
|
|
214
230
|
this.workerAppDatabase = options.workerAppDatabase ?? false;
|
|
215
231
|
this.workerAppRegistryAccountId = options.workerAppRegistryAccountId;
|
|
216
232
|
this.ownerInviteMail = options.ownerInviteMail ?? false;
|
|
@@ -796,6 +812,7 @@ export class FoundryPlanApply {
|
|
|
796
812
|
// The tunnel is gone, so what was written into it is too.
|
|
797
813
|
delete serviceIds[CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY];
|
|
798
814
|
delete serviceIds[CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY];
|
|
815
|
+
delete serviceIds[CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY];
|
|
799
816
|
}
|
|
800
817
|
if (target.system === 'cloudflare' && target.kind === 'worker') {
|
|
801
818
|
// #318: its partial-deploy traces too, or a retry after a later
|
|
@@ -1249,6 +1266,37 @@ export class FoundryPlanApply {
|
|
|
1249
1266
|
}
|
|
1250
1267
|
return results;
|
|
1251
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* The recurring audit sealing run (foundry#929): seals every ledger registered by an `audit:ledger` step with the
|
|
1271
|
+
* platform signer and has the timestamp authority stamp the new checkpoints. Foundry has no scheduler, so the host
|
|
1272
|
+
* calls this every `AUDIT_SEAL_EVERY_MINUTES` (15). One ledger failing never stops the others; each result and a
|
|
1273
|
+
* log line say what happened and why when nothing did. It writes nothing to the org's records.
|
|
1274
|
+
*/
|
|
1275
|
+
async runAuditSealTick() {
|
|
1276
|
+
const orgs = await this.store.list();
|
|
1277
|
+
const options = this.auditSealing;
|
|
1278
|
+
if (!options) {
|
|
1279
|
+
if (orgs.some((org) => org.hosting.enabled && org.hosting.entries.some((e) => e.uses?.audit === true))) {
|
|
1280
|
+
console.info('audit sealing: the host gave no auditSealing options, so no ledger is sealed');
|
|
1281
|
+
}
|
|
1282
|
+
return [];
|
|
1283
|
+
}
|
|
1284
|
+
const recorded = new Map();
|
|
1285
|
+
return runAuditSealTick({
|
|
1286
|
+
...options,
|
|
1287
|
+
orgs,
|
|
1288
|
+
now: this.now,
|
|
1289
|
+
recorded: async (org) => {
|
|
1290
|
+
const cached = recorded.get(org.id);
|
|
1291
|
+
if (cached)
|
|
1292
|
+
return cached;
|
|
1293
|
+
const ids = (await this.store.getAppliedState(org.id))?.serviceIds ?? {};
|
|
1294
|
+
recorded.set(org.id, ids);
|
|
1295
|
+
return ids;
|
|
1296
|
+
},
|
|
1297
|
+
allowed: (org) => this.authz.canApply(this.actor, org.id),
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1252
1300
|
async runOrgReleaseTick(id) {
|
|
1253
1301
|
const empty = { orgId: id, steps: [], unmanaged: [] };
|
|
1254
1302
|
if (!(await this.authz.canApply(this.actor, id))) {
|
|
@@ -1317,6 +1365,12 @@ export class FoundryPlanApply {
|
|
|
1317
1365
|
const stageSinkDown = sinkId && recorded.stage_sink_applied && declaresBlueGreen(org) && this.executor.stageSinkDown
|
|
1318
1366
|
? await this.executor.stageSinkDown(sinkId).catch(() => false)
|
|
1319
1367
|
: false;
|
|
1368
|
+
const brokerWorkspacesDatabase = org.hosting.entries.some((e) => e.uses?.auth?.workspaces)
|
|
1369
|
+
? ((await this.executor.brokerHasWorkspacesDatabase?.(org, recorded)) ?? false)
|
|
1370
|
+
: false;
|
|
1371
|
+
const provisionWorkspacesDatabase = !brokerWorkspacesDatabase && org.hosting.entries.some((e) => e.uses?.auth?.workspaces)
|
|
1372
|
+
? ((await this.executor.canProvisionWorkspacesDatabase?.(org, recorded)) ?? false)
|
|
1373
|
+
: false;
|
|
1320
1374
|
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, {
|
|
1321
1375
|
loginHostBindings: this.loginHostBindings,
|
|
1322
1376
|
loginHostClientOrganizations: this.loginHostClientOrganizations,
|
|
@@ -1326,12 +1380,15 @@ export class FoundryPlanApply {
|
|
|
1326
1380
|
zitadelRedirectUris: this.zitadelRedirectUris,
|
|
1327
1381
|
zitadelMcp: this.zitadelMcp,
|
|
1328
1382
|
zitadelOrgMetadata: this.zitadelOrgMetadata,
|
|
1383
|
+
brokerWorkspacesDatabase,
|
|
1384
|
+
provisionWorkspacesDatabase,
|
|
1329
1385
|
broker: ownsServices(org) && this.hostOwnServices && this.executor.ownBrokerPort
|
|
1330
1386
|
? await this.executor.ownBrokerPort(org, recorded)
|
|
1331
1387
|
: this.broker,
|
|
1332
1388
|
edgeWorkers: this.edgeWorkers,
|
|
1333
1389
|
workerApps: this.workerApps,
|
|
1334
1390
|
workerAppKeysUrl: this.workerAppKeysUrl,
|
|
1391
|
+
keysRegistrarUrl: this.keysRegistrarUrl,
|
|
1335
1392
|
workerAppDatabase: this.workerAppDatabase,
|
|
1336
1393
|
workerAppRegistryAccountId: this.workerAppRegistryAccountId,
|
|
1337
1394
|
ownerInviteMail: this.ownerInviteMail,
|
|
@@ -1462,6 +1519,10 @@ export class FoundryPlanApply {
|
|
|
1462
1519
|
const base = {
|
|
1463
1520
|
...(await this.sharedPlatformReadiness()),
|
|
1464
1521
|
heldEnv: await this.heldEnvReadiness(org),
|
|
1522
|
+
...(org.hosting.github ? { githubAppKeys: await this.githubAppKeysReadiness() } : {}),
|
|
1523
|
+
...(repoKeysInPlay(org, orgRecordedIds)
|
|
1524
|
+
? { repoKeys: await this.repoKeysReadiness(org, orgRecordedIds) }
|
|
1525
|
+
: {}),
|
|
1465
1526
|
};
|
|
1466
1527
|
const platform = await this.platformDeps?.store.get();
|
|
1467
1528
|
// The auth release gates the shared-host organisation check and the cookie domain of every company.
|
|
@@ -1474,6 +1535,37 @@ export class FoundryPlanApply {
|
|
|
1474
1535
|
const readiness = orgPlatformReadinessFor(org, base, orgRecordedIds, held ? await heldOwnKeyRefs(held, org) : new Set());
|
|
1475
1536
|
return withRefs(readiness);
|
|
1476
1537
|
}
|
|
1538
|
+
/** F12: asks GitHub about the repo of each entry with a deploy key to make, check or remove. Never throws; a failure is a plain line. */
|
|
1539
|
+
async repoKeysReadiness(org, recorded) {
|
|
1540
|
+
const port = this.githubDeployKeys;
|
|
1541
|
+
if (!port)
|
|
1542
|
+
return { store: false, probes: {} };
|
|
1543
|
+
const probes = {};
|
|
1544
|
+
for (const { entryKey, target } of repoKeyProbeTargets(org, recorded)) {
|
|
1545
|
+
probes[entryKey] = await probeRepoKeys(port, target);
|
|
1546
|
+
}
|
|
1547
|
+
return { store: true, probes };
|
|
1548
|
+
}
|
|
1549
|
+
/** Which refs of the platform GitHub App hold no value now. A resolver that throws counts as not held; only refs are kept. */
|
|
1550
|
+
async githubAppKeysReadiness() {
|
|
1551
|
+
const heldKeys = this.platformDeps?.heldKeys;
|
|
1552
|
+
if (!heldKeys)
|
|
1553
|
+
return { store: false, missing: [] };
|
|
1554
|
+
const missing = [];
|
|
1555
|
+
for (const ref of Object.values(PLATFORM_GITHUB_APP_REFS)) {
|
|
1556
|
+
let held;
|
|
1557
|
+
try {
|
|
1558
|
+
held = await heldKeys.resolve(ref);
|
|
1559
|
+
}
|
|
1560
|
+
catch {
|
|
1561
|
+
// Not held: the plan reports the ref, and the error is dropped.
|
|
1562
|
+
held = null;
|
|
1563
|
+
}
|
|
1564
|
+
if (!held)
|
|
1565
|
+
missing.push(ref);
|
|
1566
|
+
}
|
|
1567
|
+
return { store: true, missing };
|
|
1568
|
+
}
|
|
1477
1569
|
/**
|
|
1478
1570
|
* The org's `heldSecret` envs: the refs that hold a value now, which of those differ from the live
|
|
1479
1571
|
* production env, which could not be read, and which fit no ref. A resolver that throws counts as
|
|
@@ -1614,8 +1706,10 @@ export class FoundryPlanApply {
|
|
|
1614
1706
|
const ownerRecorded = ownerOrg
|
|
1615
1707
|
? (await this.store.getAppliedState(ownerOrg.id))?.serviceIds
|
|
1616
1708
|
: {};
|
|
1709
|
+
const jobsPublicUrl = ownerJobsPublicUrl(ownerRecorded);
|
|
1617
1710
|
return {
|
|
1618
1711
|
...readiness,
|
|
1712
|
+
...(jobsPublicUrl ? { jobsPublicUrl } : {}),
|
|
1619
1713
|
platformOwnerUserId: platformOwnerUserIdFrom(ownerOrg, ownerRecorded ?? {}),
|
|
1620
1714
|
mailKeyHeld,
|
|
1621
1715
|
canHoldSecrets: typeof heldKeys?.put === 'function',
|
|
@@ -1888,6 +1982,9 @@ export function stepEntryKey(step, orgId, entries = []) {
|
|
|
1888
1982
|
}
|
|
1889
1983
|
const entryOwned = (system === 'coolify' && kind === 'app') ||
|
|
1890
1984
|
(system === 'cloudflare' && (kind === 'worker' || kind === 'worker-app' || kind === 'gate')) ||
|
|
1985
|
+
(system === 'jobs' && kind === 'project') ||
|
|
1986
|
+
(system === 'github' && kind === 'deploy-key') ||
|
|
1987
|
+
(system === 'audit' && kind === 'ledger') ||
|
|
1891
1988
|
(kind === 'key' && (system === 'ai' || system === 'edge' || system === 'files'));
|
|
1892
1989
|
if (!entryOwned)
|
|
1893
1990
|
return null;
|
|
@@ -1914,7 +2011,8 @@ function isOrgDatabasePark(step, orgId) {
|
|
|
1914
2011
|
}
|
|
1915
2012
|
/**
|
|
1916
2013
|
* The delete of one of the org's gates that no entry declares any more. The step names only the
|
|
1917
|
-
* gate, so the caller names the removed entry's gates (`entryConfirmation.gates`)
|
|
2014
|
+
* gate, so the caller names the removed entry's gates (`entryConfirmation.gates`), or the former paths of a
|
|
2015
|
+
* remaining entry's private pages (`entryConfirmation.privatePages`), and only those pass.
|
|
1918
2016
|
*/
|
|
1919
2017
|
function isUndeclaredGateDelete(step, orgId) {
|
|
1920
2018
|
const { system, kind, key, desired } = step.action;
|
|
@@ -1923,6 +2021,10 @@ function isUndeclaredGateDelete(step, orgId) {
|
|
|
1923
2021
|
key.startsWith(`${orgId}:`) &&
|
|
1924
2022
|
desired?.mode === 'delete');
|
|
1925
2023
|
}
|
|
2024
|
+
/** Whether `gateKey` is the gate of a page the confirmed entry had (live or workspace) and has no more. */
|
|
2025
|
+
function isFormerPrivatePageGate(gateKey, confirmation) {
|
|
2026
|
+
return (confirmation.privatePages ?? []).some((path) => ['live', 'workspace'].some((scope) => privatePageGateKey(scope, confirmation.entryKey, path) === gateKey));
|
|
2027
|
+
}
|
|
1926
2028
|
/** Whether `confirmation` passes every destroying or migrating step of `steps`: all belong to its entry, and it typed the key. */
|
|
1927
2029
|
export function entryConfirms(confirmation, steps, orgId, entries) {
|
|
1928
2030
|
if (!confirmation ||
|
|
@@ -1936,8 +2038,9 @@ export function entryConfirms(confirmation, steps, orgId, entries) {
|
|
|
1936
2038
|
(isOrgDatabasePark(planned, orgId) && !entries.some(entryHasDatabase)) ||
|
|
1937
2039
|
(isUndeclaredGateDelete(planned, orgId) &&
|
|
1938
2040
|
!planned.step.migrates &&
|
|
1939
|
-
(
|
|
1940
|
-
|
|
2041
|
+
(entries.some((e) => e.key === confirmation.entryKey)
|
|
2042
|
+
? isFormerPrivatePageGate(planned.action.key.slice(orgId.length + 1), confirmation)
|
|
2043
|
+
: (confirmation.gates ?? []).includes(planned.action.key.slice(orgId.length + 1)))));
|
|
1941
2044
|
}
|
|
1942
2045
|
/** `AppliedState.serviceIds`' two flat ZITADEL scope keys (`executor.ts`'s `ensureZitadelProject`/`zitadelIssuer`) -- no `:`, unlike a resource key. */
|
|
1943
2046
|
const ZITADEL_PROJECT_SCOPE_KEY = 'zitadel_project';
|
|
@@ -1987,9 +2090,13 @@ function destroyTargets(serviceIds, orgId) {
|
|
|
1987
2090
|
'cloudflare:worker-app:',
|
|
1988
2091
|
'coolify:database:',
|
|
1989
2092
|
'coolify:app:',
|
|
2093
|
+
// The company's GitHub source, after its apps; the shared private key it uses is left alone.
|
|
2094
|
+
'coolify:github-source:',
|
|
1990
2095
|
// An own files host's storage key: the token id is the step's recorded resource id.
|
|
1991
2096
|
'coolify:own-files-r2:',
|
|
1992
2097
|
'zitadel:customers:',
|
|
2098
|
+
// The workspaces' test customers organisation.
|
|
2099
|
+
'zitadel:customers-test:',
|
|
1993
2100
|
// F6a: an entry's ai key, `<orgId>:<entry>`; the org's own key has no `:` and is left alone.
|
|
1994
2101
|
'ai:key:',
|
|
1995
2102
|
// F6b: an entry's files key, `<orgId>:<entry>`; the org's own key has no `:` and is left alone.
|
|
@@ -2083,6 +2190,21 @@ function destroyTargets(serviceIds, orgId) {
|
|
|
2083
2190
|
continue;
|
|
2084
2191
|
targets.push({ system: 'edge', kind: 'key', key: entryKey, id });
|
|
2085
2192
|
}
|
|
2193
|
+
// F7: the signed-records registrar key, live or minted and never delivered, is revoked with the org.
|
|
2194
|
+
for (const [what, scopeKey] of [
|
|
2195
|
+
['live', KEYS_REGISTRAR_SCOPE_KEY],
|
|
2196
|
+
['undelivered', KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY],
|
|
2197
|
+
]) {
|
|
2198
|
+
const id = serviceIds[scopeKey];
|
|
2199
|
+
if (id)
|
|
2200
|
+
targets.push({ system: 'coolify', kind: 'keys-registrar', key: what, id });
|
|
2201
|
+
}
|
|
2202
|
+
// A mint whose answer was lost may have left a key no id points at: the company's registrar keys all go.
|
|
2203
|
+
if (serviceIds[KEYS_REGISTRAR_SCOPE_KEY] ||
|
|
2204
|
+
serviceIds[KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY] ||
|
|
2205
|
+
serviceIds[KEYS_REGISTRAR_MINT_STARTED_SCOPE_KEY]) {
|
|
2206
|
+
targets.push({ system: 'coolify', kind: 'keys-registrar', key: 'all', id: orgId });
|
|
2207
|
+
}
|
|
2086
2208
|
// A storage key minted but never delivered has no main id; its token is deleted too.
|
|
2087
2209
|
const r2Undelivered = serviceIds[OWN_FILES_R2_UNDELIVERED_SCOPE_KEY];
|
|
2088
2210
|
if (r2Undelivered) {
|
|
@@ -2198,12 +2320,15 @@ const STEP_FIELDS = {
|
|
|
2198
2320
|
'zitadel:issuer': 'issuer.usesDomain',
|
|
2199
2321
|
'zitadel:owner': 'owners',
|
|
2200
2322
|
'zitadel:customers': 'hosting.entries.uses.auth',
|
|
2323
|
+
'zitadel:customers-test': 'hosting.entries.workspace',
|
|
2201
2324
|
'zitadel:id-token-userinfo': 'hosting.entries.uses.auth',
|
|
2202
2325
|
'zitadel:desktop-callback': 'hosting.entries.uses.auth',
|
|
2203
2326
|
'zitadel:mcp-project': 'hosting.entries.uses.auth.mcp',
|
|
2204
2327
|
'zitadel:mcp-access': 'hosting.entries.uses.auth.mcp',
|
|
2205
2328
|
'zitadel:access-token-lifetime': 'hosting.entries.uses.auth.mcp',
|
|
2206
2329
|
'coolify:broker-entry': 'hosting.entries.uses.auth',
|
|
2330
|
+
'coolify:workspace-broker-entry': 'hosting.entries.workspace',
|
|
2331
|
+
'coolify:workspaces-database': 'hosting.entries.uses.auth.workspaces',
|
|
2207
2332
|
'coolify:mail': 'mail',
|
|
2208
2333
|
'coolify:chat': 'chat.usesDomain',
|
|
2209
2334
|
'coolify:own-auth': 'services.own',
|
|
@@ -2212,6 +2337,7 @@ const STEP_FIELDS = {
|
|
|
2212
2337
|
'coolify:own-files': 'services.own',
|
|
2213
2338
|
'coolify:own-files-r2': 'services.own',
|
|
2214
2339
|
'coolify:own-jobs': 'services.own',
|
|
2340
|
+
'coolify:own-jobs-route': 'services.own',
|
|
2215
2341
|
'coolify:own-mail': 'services.own',
|
|
2216
2342
|
'coolify:own-sfu': 'services.own',
|
|
2217
2343
|
'coolify:calls': 'calls.enabled',
|
|
@@ -2221,9 +2347,12 @@ const STEP_FIELDS = {
|
|
|
2221
2347
|
'ai:key': 'ai',
|
|
2222
2348
|
'cms:site': 'site',
|
|
2223
2349
|
'coolify:app': 'hosting.enabled',
|
|
2350
|
+
'coolify:github-source': 'hosting.github',
|
|
2224
2351
|
'coolify:bluegreen': 'hosting.entries.blueGreen',
|
|
2225
2352
|
'coolify:bluegreen-lb': 'hosting.entries.blueGreen',
|
|
2226
2353
|
'coolify:bluegreen-connector': 'hosting.entries.blueGreen',
|
|
2354
|
+
'coolify:workspace': 'hosting.entries.workspace',
|
|
2355
|
+
'coolify:promote': 'hosting.entries.liveCommit',
|
|
2227
2356
|
'coolify:database': 'hosting.database',
|
|
2228
2357
|
'cloudflare:tunnel': 'hosting.tunnel',
|
|
2229
2358
|
'cloudflare:zone': 'hosting.usesDomain',
|
|
@@ -2232,6 +2361,10 @@ const STEP_FIELDS = {
|
|
|
2232
2361
|
'cloudflare:ratelimit': 'hosting.usesDomain',
|
|
2233
2362
|
'cloudflare:worker': 'hosting.entries.worker',
|
|
2234
2363
|
'cloudflare:worker-app': 'hosting.entries.workerApp',
|
|
2364
|
+
// F4 (#925): both hang off the company's Apps switch.
|
|
2365
|
+
'coolify:org-apps-admin': 'boule.apps',
|
|
2366
|
+
'cloudflare:apps-db-route': 'boule.apps',
|
|
2367
|
+
'cloudflare:apps-zones': 'boule.apps',
|
|
2235
2368
|
};
|
|
2236
2369
|
function stepField(action) {
|
|
2237
2370
|
if (action.system === 'coolify' &&
|
|
@@ -2249,6 +2382,15 @@ function stepField(action) {
|
|
|
2249
2382
|
// An entry's edge key (F7): always `<orgId>:<entry>`, edge has no org-root key.
|
|
2250
2383
|
if (action.system === 'edge' && action.kind === 'key')
|
|
2251
2384
|
return 'hosting.entries.uses.edge';
|
|
2385
|
+
// An entry's jobs project: always `<orgId>:<entry>`.
|
|
2386
|
+
if (action.system === 'jobs' && action.kind === 'project')
|
|
2387
|
+
return 'hosting.entries.uses.jobs';
|
|
2388
|
+
// An entry's audit ledger registration: always `<orgId>:<entry>`.
|
|
2389
|
+
if (action.system === 'audit' && action.kind === 'ledger')
|
|
2390
|
+
return 'hosting.entries.uses.audit';
|
|
2391
|
+
// An entry's deploy key for its own repo (F12): always `<orgId>:<entry>`.
|
|
2392
|
+
if (action.system === 'github' && action.kind === 'deploy-key')
|
|
2393
|
+
return 'hosting.entries.uses.repo';
|
|
2252
2394
|
// An org declares several issuer apps, each keyed by its name (#71).
|
|
2253
2395
|
if (action.system === 'zitadel' && action.kind === 'app') {
|
|
2254
2396
|
return `hosting.issuerApps.${action.key}`;
|
|
@@ -2257,6 +2399,10 @@ function stepField(action) {
|
|
|
2257
2399
|
if (action.system === 'cloudflare' && action.kind === 'access' && action.desired.ownedBypass) {
|
|
2258
2400
|
return 'hosting.entries.gates';
|
|
2259
2401
|
}
|
|
2402
|
+
// F11: a bypass for a machine path of a gated entry.
|
|
2403
|
+
if (action.system === 'cloudflare' && action.kind === 'access' && action.desired.machineBypass) {
|
|
2404
|
+
return 'hosting.entries.uses';
|
|
2405
|
+
}
|
|
2260
2406
|
return STEP_FIELDS[`${action.system}:${action.kind}`] ?? `${action.system}.${action.kind}`;
|
|
2261
2407
|
}
|
|
2262
2408
|
/**
|
|
@@ -2312,10 +2458,18 @@ function failedFields(org, detail, completed, serviceIds) {
|
|
|
2312
2458
|
failed.add('hosting.entries.uses.edge');
|
|
2313
2459
|
}
|
|
2314
2460
|
}
|
|
2315
|
-
// F3: Boule's own key, wanted whenever the org has a domain.
|
|
2316
|
-
if (org
|
|
2461
|
+
// F3: Boule's own key, wanted whenever the org has a domain or its Boule runs apps.
|
|
2462
|
+
if (bouleEdgeKeyWanted(org) && serviceIds[entryEdgeUndeliveredScopeKey(BOULE_EDGE_ENTRY_KEY)]) {
|
|
2317
2463
|
failed.add('hosting.entries.uses.edge');
|
|
2318
2464
|
}
|
|
2465
|
+
// F7: a registrar key minted and never delivered stays unresolved until a later apply revokes it.
|
|
2466
|
+
if (serviceIds[KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY])
|
|
2467
|
+
failed.add('boule.apps');
|
|
2468
|
+
// F4 (#925): the same for an apps admin role or a route service token minted and never delivered.
|
|
2469
|
+
if (serviceIds[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY] ||
|
|
2470
|
+
serviceIds[remoteDatabaseKeys(APPS_DB_ENTRY_KEY).undelivered]) {
|
|
2471
|
+
failed.add('boule.apps');
|
|
2472
|
+
}
|
|
2319
2473
|
// The same for a jobs registration whose credentials never reached the host.
|
|
2320
2474
|
if (serviceIds[JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY] &&
|
|
2321
2475
|
!serviceIds[`jobs:registration:${org.id}`]) {
|
|
@@ -1416,8 +1416,8 @@ export declare const JOBS_SCHEDULER_KEY = "jobs";
|
|
|
1416
1416
|
/**
|
|
1417
1417
|
* The scheduler's address on the Coolify network (`http://<custom_internal_name>:8080`),
|
|
1418
1418
|
* the value the org steps' jobs port and Archon's `JOBS_URL` are read from.
|
|
1419
|
-
* Not a secret.
|
|
1420
|
-
*
|
|
1419
|
+
* Not a secret. Only containers on that network reach it; an entry's app uses
|
|
1420
|
+
* the owner company's public origin `JOBS_PUBLIC_URL_SCOPE_KEY` (`own-jobs-route.ts`) instead.
|
|
1421
1421
|
*/
|
|
1422
1422
|
export declare const JOBS_INTERNAL_URL_SCOPE_KEY = "jobs_internal_url";
|
|
1423
1423
|
/**
|