@wtfalch/foundry 0.60.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +7 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/native-build.d.ts +70 -0
- package/dist/native-build.js +92 -0
- package/dist/org.d.ts +184 -24
- package/dist/org.js +444 -49
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +60 -8
- package/dist/plan/blue-green.js +115 -10
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +15 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +552 -104
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-access.js +1 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +231 -19
- package/dist/plan/executor.js +1648 -129
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +7 -4
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +15 -2
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +13 -2
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +12 -3
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +20 -7
- package/dist/store/postgres.js +25 -2
- package/dist/store/schema.d.ts +73 -1
- package/dist/store/schema.js +10 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +19 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DeclaredOrg, type HostingEntry } from '../org.js';
|
|
2
|
-
import type { DeclaredOrgStore, PlatformStore } from '../store.js';
|
|
2
|
+
import type { DeclaredOrgStore, HeldLease, MutationLease, PlatformStore } from '../store.js';
|
|
3
3
|
import type { CoolifyPort } from './executor.js';
|
|
4
4
|
/**
|
|
5
5
|
* F5 (https://github.com/wtfalch/foundry/issues/314): an entry with
|
|
@@ -62,6 +62,11 @@ export interface BrokerEntryWant {
|
|
|
62
62
|
loginHost: string;
|
|
63
63
|
onboardingUrl?: string;
|
|
64
64
|
};
|
|
65
|
+
/** Present only for an entry with `uses.auth.workspaces`. The key's hash is not part of it: see {@link writeBrokerEntry}. */
|
|
66
|
+
workspaces?: {
|
|
67
|
+
roles: string[];
|
|
68
|
+
inviteUrl: string;
|
|
69
|
+
};
|
|
65
70
|
/**
|
|
66
71
|
* `true` for `signIn: 'email-code'`, `false` for an entry that declares `signIn: 'password'`.
|
|
67
72
|
* Absent when the entry declares no `signIn`: a hand-set `emailLink` stays as it is.
|
|
@@ -95,10 +100,22 @@ export interface BrokerPort {
|
|
|
95
100
|
is_preview: boolean;
|
|
96
101
|
}>>;
|
|
97
102
|
setAuthApps(raw: string): Promise<void>;
|
|
103
|
+
/** Sets one env of the broker app. Needed only to give the broker its `WORKSPACES_DATABASE_URL`. */
|
|
104
|
+
setEnv?(key: string, value: string): Promise<void>;
|
|
98
105
|
deploy(): Promise<void>;
|
|
99
106
|
/** The platform's `PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY` value, if any. */
|
|
100
107
|
recordedDeploy(): Promise<string | undefined>;
|
|
101
|
-
|
|
108
|
+
/** With `fence` (a `platform` lease the caller holds, from {@link withPlatformLease}) the write carries it and takes no lease itself. */
|
|
109
|
+
recordDeploy(value: string, fence?: MutationLease): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* T3b: runs `fn` holding the `platform` mutation lease, so two companies' applies never read, change and
|
|
112
|
+
* write a shared broker env (`AUTH_REGISTRARS`) at once. `fn` gets the lease (undefined for a port with
|
|
113
|
+
* no lease store), which its `recordDeploy` carries. Returns {@link HeldLease} while another holder has
|
|
114
|
+
* it, and `fn` does not run. Optional: a port without it writes unleased.
|
|
115
|
+
*/
|
|
116
|
+
withPlatformLease?<T>(fn: (fence: MutationLease | undefined) => Promise<T>): Promise<{
|
|
117
|
+
value: T;
|
|
118
|
+
} | HeldLease>;
|
|
102
119
|
/**
|
|
103
120
|
* The id of Archon's own sign-in organisation, when the platform has made it.
|
|
104
121
|
* No other entry may sign in that organisation, so an entry that does is
|
|
@@ -129,10 +146,14 @@ export declare class PlatformBrokerPort implements BrokerPort {
|
|
|
129
146
|
is_preview: boolean;
|
|
130
147
|
}[]>;
|
|
131
148
|
setAuthApps(raw: string): Promise<void>;
|
|
149
|
+
setEnv(key: string, value: string): Promise<void>;
|
|
132
150
|
deploy(): Promise<void>;
|
|
133
151
|
recordedDeploy(): Promise<string | undefined>;
|
|
134
152
|
archonOrganisationId(): Promise<string | undefined>;
|
|
135
|
-
|
|
153
|
+
withPlatformLease<T>(fn: (fence: MutationLease | undefined) => Promise<T>): Promise<{
|
|
154
|
+
value: T;
|
|
155
|
+
} | HeldLease>;
|
|
156
|
+
recordDeploy(value: string, fence?: MutationLease): Promise<void>;
|
|
136
157
|
}
|
|
137
158
|
/**
|
|
138
159
|
* Writes one entry's broker entry beside the others and deploys the broker
|
|
@@ -149,6 +170,10 @@ export declare function writeBrokerEntry(broker: BrokerPort, input: {
|
|
|
149
170
|
want: BrokerEntryWant;
|
|
150
171
|
appKey: string;
|
|
151
172
|
previous: BrokerEntryWant | null;
|
|
173
|
+
/** sha256 hex of the entry's workspaces key. Required with `want.workspaces`; the key itself never reaches here. */
|
|
174
|
+
workspacesKeyHash?: string;
|
|
175
|
+
/** The operator's held connection string for the broker's workspaces database, written to the broker when it differs. */
|
|
176
|
+
workspacesDatabaseUrl?: string;
|
|
152
177
|
}): Promise<{
|
|
153
178
|
invalidForeign: string[];
|
|
154
179
|
replaced: string | null;
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { isFoundryOwnedBrokerEntryName, } from '../org.js';
|
|
3
3
|
import { entryClientIdScopeKey } from './agora.js';
|
|
4
4
|
import { MCP_DATABASE_URL_ENV, MCP_OAUTH_SECRET_ENV } from './broker-mcp.js';
|
|
5
|
+
import { BROKER_WORKSPACES_DATABASE_ENV, wantedWorkspaces, workspacesUrl, } from './entry-workspaces.js';
|
|
5
6
|
import { entryHostnames } from './hostnames.js';
|
|
6
7
|
import { loginHostnameFor } from './login-host.js';
|
|
7
8
|
import { PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, parseBrokerDeployRecord, } from './platform-diff.js';
|
|
@@ -80,6 +81,8 @@ export function entryAuthEnv(org, entry, recorded, issuer) {
|
|
|
80
81
|
}
|
|
81
82
|
: {};
|
|
82
83
|
Object.assign(values, mcpValues);
|
|
84
|
+
if (auth.workspaces)
|
|
85
|
+
Object.assign(values, { WORKSPACES_URL: workspacesUrl(org) });
|
|
83
86
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => Boolean(value)));
|
|
84
87
|
}
|
|
85
88
|
/** The marker `pushAppEnv` records for an entry's sign-in env; the planner compares against it. */
|
|
@@ -105,8 +108,17 @@ export function wantedBrokerEntry(org, entry, recorded) {
|
|
|
105
108
|
if (auth.mcp && !mcp) {
|
|
106
109
|
missing.push({ what: 'its MCP audience project id', producedBy: ['zitadel', 'mcp-project'] });
|
|
107
110
|
}
|
|
108
|
-
|
|
111
|
+
const workspaces = wantedWorkspaces(org, entry);
|
|
112
|
+
if (auth.workspaces && !workspaces) {
|
|
113
|
+
missing.push({ what: 'its public address', producedBy: ['coolify', 'app'] });
|
|
114
|
+
}
|
|
115
|
+
if (!organizationId ||
|
|
116
|
+
!clientId ||
|
|
117
|
+
!mailDomain ||
|
|
118
|
+
(auth.mcp && !mcp) ||
|
|
119
|
+
(auth.workspaces && !workspaces)) {
|
|
109
120
|
return { missing };
|
|
121
|
+
}
|
|
110
122
|
return {
|
|
111
123
|
want: {
|
|
112
124
|
name: brokerEntryName(org, entry),
|
|
@@ -117,10 +129,13 @@ export function wantedBrokerEntry(org, entry, recorded) {
|
|
|
117
129
|
registration: auth.registration === 'open',
|
|
118
130
|
replaces: auth.replacesBrokerEntry ?? null,
|
|
119
131
|
...(mcp ? { mcp } : {}),
|
|
132
|
+
...(workspaces ? { workspaces } : {}),
|
|
120
133
|
...(auth.signIn ? { emailLink: auth.signIn === 'email-code' } : {}),
|
|
121
134
|
},
|
|
122
135
|
};
|
|
123
136
|
}
|
|
137
|
+
/** How long {@link PlatformBrokerPort.withPlatformLease} holds the `platform` lease: an env write, two reads, a deploy call and a record. */
|
|
138
|
+
const PLATFORM_LEASE_TTL_MS = 120_000;
|
|
124
139
|
/** {@link BrokerPort} over the platform's recorded ids and a Coolify client. */
|
|
125
140
|
export class PlatformBrokerPort {
|
|
126
141
|
options;
|
|
@@ -141,6 +156,9 @@ export class PlatformBrokerPort {
|
|
|
141
156
|
{ key: 'AUTH_APPS', value: raw },
|
|
142
157
|
]);
|
|
143
158
|
}
|
|
159
|
+
async setEnv(key, value) {
|
|
160
|
+
await this.options.coolify.bulkUpsertEnvs(await this.appId(), [{ key, value }]);
|
|
161
|
+
}
|
|
144
162
|
async deploy() {
|
|
145
163
|
await this.options.coolify.deploy(await this.appId());
|
|
146
164
|
}
|
|
@@ -150,8 +168,27 @@ export class PlatformBrokerPort {
|
|
|
150
168
|
async archonOrganisationId() {
|
|
151
169
|
return (await this.options.store.getServiceIds())[PLATFORM_ARCHON_ORG_SCOPE_KEY] || undefined;
|
|
152
170
|
}
|
|
153
|
-
async
|
|
171
|
+
async withPlatformLease(fn) {
|
|
172
|
+
const { leases } = this.options;
|
|
173
|
+
if (!leases)
|
|
174
|
+
return { value: await fn(undefined) };
|
|
175
|
+
const lease = await leases.acquireMutationLease('platform', randomUUID(), PLATFORM_LEASE_TTL_MS);
|
|
176
|
+
if (!('token' in lease))
|
|
177
|
+
return lease;
|
|
178
|
+
try {
|
|
179
|
+
return { value: await fn(lease) };
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await leases.releaseMutationLease(lease).catch(() => undefined);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async recordDeploy(value, fence) {
|
|
154
186
|
const { leases, store } = this.options;
|
|
187
|
+
if (fence) {
|
|
188
|
+
const ids = await store.getServiceIds();
|
|
189
|
+
await store.putServiceIds({ ...ids, [PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY]: value }, fence);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
155
192
|
const lease = await leases?.acquireMutationLease('platform', randomUUID(), 60_000);
|
|
156
193
|
if (lease && !('token' in lease))
|
|
157
194
|
return;
|
|
@@ -187,6 +224,24 @@ export async function writeBrokerEntry(broker, input) {
|
|
|
187
224
|
throw new Error(`${label} would sign in Archon's own organisation, which no other entry may`);
|
|
188
225
|
}
|
|
189
226
|
const brokerEnvs = await broker.listEnvs();
|
|
227
|
+
// The broker does not start without the database once an entry declares workspaces, and then no one signs in.
|
|
228
|
+
let pendingDatabase = null;
|
|
229
|
+
if (want.workspaces) {
|
|
230
|
+
if (!input.workspacesKeyHash) {
|
|
231
|
+
throw new Error(`${label}: the workspaces key hash is missing, so nothing was written`);
|
|
232
|
+
}
|
|
233
|
+
const liveDatabase = brokerEnvs.find((e) => e.key === BROKER_WORKSPACES_DATABASE_ENV && !e.is_preview)?.value;
|
|
234
|
+
const wanted = input.workspacesDatabaseUrl ?? liveDatabase;
|
|
235
|
+
if (!wanted) {
|
|
236
|
+
throw new Error(`${label}: the broker has no ${BROKER_WORKSPACES_DATABASE_ENV}, so nothing was written`);
|
|
237
|
+
}
|
|
238
|
+
if (wanted !== liveDatabase) {
|
|
239
|
+
if (!broker.setEnv) {
|
|
240
|
+
throw new Error(`${label}: the broker port cannot set ${BROKER_WORKSPACES_DATABASE_ENV}`);
|
|
241
|
+
}
|
|
242
|
+
pendingDatabase = wanted;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
190
245
|
const env = brokerEnvs.find((e) => e.key === 'AUTH_APPS' && !e.is_preview);
|
|
191
246
|
// A value that reads back empty is not an empty list: writing over it
|
|
192
247
|
// would drop every hand-written entry.
|
|
@@ -235,6 +290,15 @@ export async function writeBrokerEntry(broker, input) {
|
|
|
235
290
|
entry.mcp = want.mcp;
|
|
236
291
|
else
|
|
237
292
|
entry.mcp = undefined;
|
|
293
|
+
// The previous write's `workspaces` goes when the entry no longer declares it; the key hash goes with it.
|
|
294
|
+
if (want.workspaces && input.workspacesKeyHash) {
|
|
295
|
+
entry.workspaces = want.workspaces;
|
|
296
|
+
entry.workspacesKey = input.workspacesKeyHash;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
entry.workspaces = undefined;
|
|
300
|
+
entry.workspacesKey = undefined;
|
|
301
|
+
}
|
|
238
302
|
// A hand-set `emailLink` stays unless the entry declares `signIn`, or declared it before and stopped.
|
|
239
303
|
if (want.emailLink !== undefined)
|
|
240
304
|
entry.emailLink = want.emailLink;
|
|
@@ -275,6 +339,12 @@ export async function writeBrokerEntry(broker, input) {
|
|
|
275
339
|
next.push(existing);
|
|
276
340
|
});
|
|
277
341
|
place();
|
|
342
|
+
// The database first: a failure after it leaves a broker env nothing reads yet, never an entry the broker cannot start with.
|
|
343
|
+
let databaseWritten = false;
|
|
344
|
+
if (pendingDatabase !== null) {
|
|
345
|
+
await broker.setEnv?.(BROKER_WORKSPACES_DATABASE_ENV, pendingDatabase);
|
|
346
|
+
databaseWritten = true;
|
|
347
|
+
}
|
|
278
348
|
const nextRaw = JSON.stringify(next);
|
|
279
349
|
const changed = JSON.stringify(current) !== nextRaw;
|
|
280
350
|
if (changed) {
|
|
@@ -291,7 +361,8 @@ export async function writeBrokerEntry(broker, input) {
|
|
|
291
361
|
// so that one does not hold back the deploy.
|
|
292
362
|
const missing = (await missingBrokerInputs({ listEnvs: () => broker.listEnvs() }, brokerAppId)).filter((input) => input !== 'AUTH_APPS');
|
|
293
363
|
const live = changed ? nextRaw : raw;
|
|
294
|
-
if (
|
|
364
|
+
if (databaseWritten ||
|
|
365
|
+
brokerDeployNeeded(missing, brokerAppId, live, parseBrokerDeployRecord(await broker.recordedDeploy()))) {
|
|
295
366
|
await broker.deploy();
|
|
296
367
|
// The platform's record of what the broker last deployed, so its next
|
|
297
368
|
// apply does not read this write as drift and deploy the broker again.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
|
+
import type { BrokerEntryInput, BrokerPort } from './broker-entries.js';
|
|
3
|
+
/**
|
|
4
|
+
* T3 (campaign 2026-10-20, apps-modules): a company with customers gets one registrar key at the sign-in
|
|
5
|
+
* broker, so its Boule can register the company's own apps there (`PUT /apps/:organizationId/:name`, auth ADR
|
|
6
|
+
* 0001 and 0003) without Archon. Foundry makes the key, holds it at {@link registrarRef}, writes the
|
|
7
|
+
* company's record into the broker's `AUTH_REGISTRARS` (`broker/src/apps.ts`'s `parseRegistrars`: the key
|
|
8
|
+
* itself, the company's sign-in organisation, its customers organisations, its login host) and gives Boule
|
|
9
|
+
* `AUTH_REGISTRAR_URL` (the broker's base, `<issuer>/api`, the address `@wtfalch/auth` already calls) and
|
|
10
|
+
* `AUTH_REGISTRAR_KEY`.
|
|
11
|
+
*
|
|
12
|
+
* Two steps deliver it, and either may run first: the Boule step (`coolify:boule`) puts the key in Boule's
|
|
13
|
+
* env, the registrar step (`coolify:broker-registrar`) puts the record in the broker's. Each mint gets an id
|
|
14
|
+
* (never the key) recorded at {@link REGISTRAR_MINTED_SCOPE_KEY}; each step records which mint it delivered,
|
|
15
|
+
* so a key made again reaches both places. The plan never holds the key.
|
|
16
|
+
*/
|
|
17
|
+
export declare const AUTH_REGISTRARS_ENV = "AUTH_REGISTRARS";
|
|
18
|
+
export { AUTH_REGISTRAR_KEY_ENV, AUTH_REGISTRAR_URL_ENV, registrarUrl, registrarWanted, } from './boule.js';
|
|
19
|
+
/** The id of the held key's mint (a random id, not the key), set once it is held. Cleared when the key is forgotten. */
|
|
20
|
+
export declare const REGISTRAR_MINTED_SCOPE_KEY = "registrar_minted";
|
|
21
|
+
/** The mint id whose key the last successful Boule env write carried. */
|
|
22
|
+
export declare const REGISTRAR_DELIVERED_SCOPE_KEY = "registrar_delivered";
|
|
23
|
+
/** What the last successful broker write said (JSON of {@link RegistrarRecord} plus the mint id). Never the key. */
|
|
24
|
+
export declare const REGISTRAR_APPLIED_SCOPE_KEY = "broker_registrar_applied";
|
|
25
|
+
/** The broker inputs that were missing when the record was written, so the broker was not deployed with it. */
|
|
26
|
+
export declare const REGISTRAR_DEPLOY_WAITS_SCOPE_KEY = "broker_registrar_deploy_waits";
|
|
27
|
+
/** Why the last removal wrote nothing (another company's record the broker refuses), so the key was kept. Never a key. */
|
|
28
|
+
export declare const REGISTRAR_REMOVAL_HELD_SCOPE_KEY = "broker_registrar_removal_held";
|
|
29
|
+
/** The held-key ref of a company's registrar key. */
|
|
30
|
+
export declare const registrarRef: (orgId: string) => string;
|
|
31
|
+
/** 32 random bytes, base64url: 43 characters, over the broker's 32-character floor. */
|
|
32
|
+
export declare const mintRegistrarKey: () => string;
|
|
33
|
+
interface HeldKeys {
|
|
34
|
+
resolve(ref: string): Promise<string | null | undefined>;
|
|
35
|
+
put?(ref: string, value: string): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The company's registrar key: read from the held keys, else made and held first, and its mint id recorded
|
|
39
|
+
* and persisted before the key goes anywhere, so a crash after the `put` hands the same key out next time. A
|
|
40
|
+
* mint recorded but no longer held (F5, Q36: the held value was lost) is made again by this step, never by a
|
|
41
|
+
* person removing a record: the new mint id is recorded and persisted BEFORE the new key is held, so a crash
|
|
42
|
+
* between the two leaves a mint nobody was given and the next run makes another, and a crash after the `put`
|
|
43
|
+
* hands the held key out. The mint id differs from the lost one, so the Boule step and the broker step (each
|
|
44
|
+
* records which mint it delivered) both deliver the new key; the broker's write replaces the company's old
|
|
45
|
+
* record by its organisation. Errors name the ref, never the key. `secret` registers it for redaction; `log`
|
|
46
|
+
* gets one plain line when a lost key is made again.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ensureRegistrarKey(input: {
|
|
49
|
+
orgId: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
heldKeys: HeldKeys | undefined;
|
|
52
|
+
recorded: () => Readonly<Record<string, string>>;
|
|
53
|
+
record: (key: string, value: string) => void;
|
|
54
|
+
persist: () => Promise<void>;
|
|
55
|
+
secret: (value: string) => void;
|
|
56
|
+
log?: (line: string) => void;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
key: string;
|
|
59
|
+
mint: string;
|
|
60
|
+
}>;
|
|
61
|
+
/** The company's record in `AUTH_REGISTRARS`, without its key. */
|
|
62
|
+
export interface RegistrarRecord {
|
|
63
|
+
/** The company's own sign-in organisation (its Boule's `AUTH_ORGANIZATION_ID`). */
|
|
64
|
+
organizationId: string;
|
|
65
|
+
/** Its customers organisation, then its test customers organisation once that exists. */
|
|
66
|
+
customers: string[];
|
|
67
|
+
loginHost: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* What the company's record says, or each input still missing and the step that records it. The customers
|
|
71
|
+
* organisation is an input only while `customers.enabled`: a company whose Boule runs apps (F5) without
|
|
72
|
+
* customers has a record with no customers, and the broker registers its apps against the sign-in organisation.
|
|
73
|
+
*/
|
|
74
|
+
export declare function wantedRegistrar(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): {
|
|
75
|
+
want: RegistrarRecord;
|
|
76
|
+
} | {
|
|
77
|
+
missing: BrokerEntryInput[];
|
|
78
|
+
};
|
|
79
|
+
/** The applied marker: the record and the mint it carries the key of. */
|
|
80
|
+
export declare const registrarAppliedValue: (record: RegistrarRecord, mint: string) => string;
|
|
81
|
+
/** The organisation id the last broker write named, or null. */
|
|
82
|
+
export declare function appliedRegistrarOrganization(recorded: Readonly<Record<string, string>>): string | null;
|
|
83
|
+
/**
|
|
84
|
+
* `null` when the broker's `parseRegistrars` takes `list`, else why not, naming the entry by its place and
|
|
85
|
+
* organisation, never its key. The broker stops at boot on a list it refuses, and then nobody signs in, so
|
|
86
|
+
* nothing is written that it would refuse.
|
|
87
|
+
*/
|
|
88
|
+
export declare function registrarsProblem(list: readonly unknown[],
|
|
89
|
+
/** How an entry is named; by default its place in `list`. */
|
|
90
|
+
labelOf?: (index: number) => string): string | null;
|
|
91
|
+
/** What {@link writeRegistrar} did: whether the value changed, what the broker waits for, and why a removal wrote nothing. */
|
|
92
|
+
export interface RegistrarWrite {
|
|
93
|
+
changed: boolean;
|
|
94
|
+
waits: string[];
|
|
95
|
+
/**
|
|
96
|
+
* A removal that wrote nothing because another company's record in the list is one the broker refuses
|
|
97
|
+
* (it would not start on that list with or without this company's record). The company's record stays,
|
|
98
|
+
* so its key is kept. Names the entry by its place, never a key.
|
|
99
|
+
*/
|
|
100
|
+
removalHeld?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Puts the company's record (or, with `record` null, takes it away) in the broker's `AUTH_REGISTRARS`,
|
|
104
|
+
* beside every other company's record, which is copied back as it was. The company's own record is the one
|
|
105
|
+
* naming its organisation, the organisation the last write named, any of `ownOrganizationIds`, or the one
|
|
106
|
+
* carrying `ownKey`. The read, change, write and deploy run under the `platform` lease (`BrokerPort`'s
|
|
107
|
+
* `withPlatformLease`), and the value is read again after the write: a record not as written fails the
|
|
108
|
+
* step. Deploys the broker when the value changed and the broker has its other inputs; otherwise returns
|
|
109
|
+
* the inputs it waits for, and the broker reads the record on its next deploy. Errors name the company and
|
|
110
|
+
* the reason, never a key.
|
|
111
|
+
*/
|
|
112
|
+
export declare function writeRegistrar(broker: BrokerPort, input: {
|
|
113
|
+
slug: string;
|
|
114
|
+
record: (RegistrarRecord & {
|
|
115
|
+
key: string;
|
|
116
|
+
}) | null;
|
|
117
|
+
previousOrganizationId: string | null;
|
|
118
|
+
/** More organisations whose record is this company's (its recorded sign-in organisation), for a removal after a crash before the applied marker. */
|
|
119
|
+
ownOrganizationIds?: readonly string[];
|
|
120
|
+
/** The company's held key: a record carrying it is the company's whatever organisation it names. */
|
|
121
|
+
ownKey?: string | null;
|
|
122
|
+
/** Told every key it reads or writes (every company's), so the caller can redact them from an error. */
|
|
123
|
+
secret?: (value: string) => void;
|
|
124
|
+
/** One plain line per thing worth a log (a replaced hand-written record, an unleased write). Never a key. */
|
|
125
|
+
log?: (line: string) => void;
|
|
126
|
+
}): Promise<RegistrarWrite>;
|
|
127
|
+
/** A planned step, as `diff.ts` collects them. */
|
|
128
|
+
interface RegistrarDraftStep {
|
|
129
|
+
action: {
|
|
130
|
+
system: 'coolify';
|
|
131
|
+
kind: string;
|
|
132
|
+
key: string;
|
|
133
|
+
desired: Record<string, unknown>;
|
|
134
|
+
};
|
|
135
|
+
step: {
|
|
136
|
+
description: string;
|
|
137
|
+
destroys: boolean;
|
|
138
|
+
migrates: boolean;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The registrar for an org: whether the Boule step delivers the key (`deliver`), the broker step, and every
|
|
143
|
+
* reason either is held back as a plan line. Nothing at all for a company that never had customers, so
|
|
144
|
+
* its plan is unchanged.
|
|
145
|
+
*/
|
|
146
|
+
export declare function planBrokerRegistrar(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, plannedSteps: ReadonlyArray<{
|
|
147
|
+
action: {
|
|
148
|
+
system: string;
|
|
149
|
+
kind: string;
|
|
150
|
+
};
|
|
151
|
+
}>, host: {
|
|
152
|
+
brokerWired: boolean;
|
|
153
|
+
canHoldSecrets: boolean;
|
|
154
|
+
issuerKnown: boolean;
|
|
155
|
+
}): {
|
|
156
|
+
deliver: boolean;
|
|
157
|
+
/** Customers are off but the record cannot be taken away yet: Boule keeps its registrar env, in step with it. */
|
|
158
|
+
keepBouleEnv: boolean;
|
|
159
|
+
step: RegistrarDraftStep | null;
|
|
160
|
+
blocked: string[];
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* The Boule env drift the registrar adds: the key missing, or not the mint the held key is (made again since
|
|
164
|
+
* Boule got it). And, once customers are off, the rows foundry wrote, which the Boule step takes away.
|
|
165
|
+
*/
|
|
166
|
+
export declare function bouleRegistrarDrift(org: Pick<DeclaredOrg, 'customers' | 'boule'>, recorded: Readonly<Record<string, string>>, liveEnv: Readonly<Record<string, string>>, deliver: boolean,
|
|
167
|
+
/** {@link planBrokerRegistrar}'s `keepBouleEnv`: the record stays at the broker, so Boule keeps the key. */
|
|
168
|
+
keep?: boolean): string[];
|