@wtfalch/foundry 0.45.1 → 0.47.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/build-json.d.ts +28 -0
- package/dist/build-json.js +95 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +6 -2
- package/dist/migrations/0058_app_requests.sql +32 -0
- package/dist/org.d.ts +68 -5
- package/dist/org.js +287 -135
- package/dist/plan/agora.d.ts +14 -0
- package/dist/plan/agora.js +21 -0
- package/dist/plan/backup-gate.d.ts +15 -0
- package/dist/plan/backup-gate.js +38 -0
- package/dist/plan/boule-owners-http.d.ts +12 -0
- package/dist/plan/boule-owners-http.js +59 -0
- package/dist/plan/boule.d.ts +38 -2
- package/dist/plan/boule.js +72 -1
- package/dist/plan/company-backups.d.ts +7 -1
- package/dist/plan/company-backups.js +42 -2
- package/dist/plan/cookie-domain.d.ts +41 -0
- package/dist/plan/cookie-domain.js +71 -0
- package/dist/plan/desktop-auth.d.ts +58 -0
- package/dist/plan/desktop-auth.js +144 -0
- package/dist/plan/desktop-callback.d.ts +26 -0
- package/dist/plan/desktop-callback.js +59 -0
- package/dist/plan/diff.d.ts +13 -4
- package/dist/plan/diff.js +277 -88
- package/dist/plan/executor.d.ts +136 -6
- package/dist/plan/executor.js +732 -105
- package/dist/plan/health-gaps.js +5 -0
- package/dist/plan/health-targets.js +1 -2
- package/dist/plan/hostnames.d.ts +2 -2
- package/dist/plan/hostnames.js +6 -6
- package/dist/plan/index.d.ts +4 -1
- package/dist/plan/index.js +3 -1
- package/dist/plan/job-app.d.ts +23 -0
- package/dist/plan/job-app.js +41 -0
- package/dist/plan/live.d.ts +6 -0
- package/dist/plan/live.js +16 -0
- package/dist/plan/login-host.d.ts +28 -0
- package/dist/plan/login-host.js +36 -0
- package/dist/plan/org-names.js +9 -1
- package/dist/plan/org-postgres-job.d.ts +22 -0
- package/dist/plan/org-postgres-job.js +91 -0
- package/dist/plan/org-postgres-migrate.d.ts +115 -0
- package/dist/plan/org-postgres-migrate.js +466 -0
- package/dist/plan/org-postgres.d.ts +90 -0
- package/dist/plan/org-postgres.js +293 -0
- package/dist/plan/plan-apply.d.ts +60 -3
- package/dist/plan/plan-apply.js +174 -8
- package/dist/plan/platform-diff.d.ts +45 -0
- package/dist/plan/platform-diff.js +159 -4
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-org.js +4 -0
- package/dist/plan/platform-providers.d.ts +12 -2
- package/dist/plan/platform-providers.js +282 -9
- package/dist/plan/post-logout-uris.d.ts +22 -0
- package/dist/plan/post-logout-uris.js +43 -0
- package/dist/plan/providers-readers.d.ts +10 -1
- package/dist/plan/providers-readers.js +56 -5
- package/dist/plan/sign-in-bindings.d.ts +4 -0
- package/dist/plan/sign-in-bindings.js +10 -0
- package/dist/plan/sign-in-org.d.ts +19 -6
- package/dist/plan/sign-in-org.js +35 -10
- package/dist/plan/sign-in.d.ts +12 -5
- package/dist/plan/sign-in.js +12 -5
- package/dist/plan/signin-db-move.js +10 -34
- package/dist/plan/worker-app.js +3 -1
- package/dist/refuse-org.d.ts +7 -0
- package/dist/refuse-org.js +10 -0
- package/dist/requested-changes.d.ts +50 -4
- package/dist/requested-changes.js +9 -1
- package/dist/store/postgres.d.ts +5 -1
- package/dist/store/postgres.js +67 -8
- package/dist/store/schema.d.ts +72 -0
- package/dist/store/schema.js +4 -0
- package/dist/store.d.ts +24 -1
- package/dist/worker-app-config.d.ts +8 -2
- package/dist/worker-app-config.js +20 -6
- package/package.json +5 -5
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, orgServiceDatabase, } from '../org.js';
|
|
3
|
+
import { JobFailure, waitForJob } from './job-app.js';
|
|
4
|
+
import { ORG_POSTGRES_JOB_ADMIN_ENV, jobCreatesRuntimeRole, orgPostgresPasswordEnv, orgPostgresProvisionDockerfile, orgPostgresRuntimePasswordEnv, } from './org-postgres-job.js';
|
|
5
|
+
import { boundedCall, withCallTimeout } from './signin-db.js';
|
|
6
|
+
/** Recorded once a live company's data for `home` is copied to the org's server (#444). */
|
|
7
|
+
export const orgPostgresMigratedScopeKey = (home) => `org_postgres_migrated.${home}`;
|
|
8
|
+
/** Whether `home` still has a legacy Coolify database that has not been moved to the server. */
|
|
9
|
+
export function legacyUnmigrated(org, home, recordedIds) {
|
|
10
|
+
const legacy = home.legacyKey(org);
|
|
11
|
+
return (legacy !== null &&
|
|
12
|
+
Boolean(recordedIds[legacy]) &&
|
|
13
|
+
!recordedIds[orgPostgresMigratedScopeKey(home.schema)]);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Pure. Where a consumer finds its database, from recorded ids alone: on the
|
|
17
|
+
* server once the home is recorded and either it never had a legacy database
|
|
18
|
+
* or its migration is recorded. A live company stays on its Coolify database
|
|
19
|
+
* until then.
|
|
20
|
+
*/
|
|
21
|
+
export function serviceOnServer(org, home, recordedIds) {
|
|
22
|
+
return Boolean(recordedIds[home.scopeKey]) && !legacyUnmigrated(org, home, recordedIds);
|
|
23
|
+
}
|
|
24
|
+
/** Pure. The home's endpoint on the server whose Coolify `internal_db_url` is given; null when that URL has no host. */
|
|
25
|
+
export function serviceEndpoint(org, home, serverInternalUrl) {
|
|
26
|
+
if (!URL.canParse(serverInternalUrl))
|
|
27
|
+
return null;
|
|
28
|
+
const url = new URL(serverInternalUrl);
|
|
29
|
+
if (!url.hostname)
|
|
30
|
+
return null;
|
|
31
|
+
const names = orgServiceDatabase(org, home);
|
|
32
|
+
return {
|
|
33
|
+
host: url.hostname,
|
|
34
|
+
port: url.port || '5432',
|
|
35
|
+
database: names.database,
|
|
36
|
+
ownerUser: names.ownerRole,
|
|
37
|
+
runtimeUser: names.runtimeRole,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Pure. The owner URL, and the runtime URL when a runtime password is given. */
|
|
41
|
+
export function serviceUrls(endpoint, ownerPassword, runtimePassword) {
|
|
42
|
+
const build = (user, password) => `postgres://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${endpoint.host}:${endpoint.port}/${endpoint.database}`;
|
|
43
|
+
return {
|
|
44
|
+
owner: build(endpoint.ownerUser, ownerPassword),
|
|
45
|
+
runtime: runtimePassword === null ? null : build(endpoint.runtimeUser, runtimePassword),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Restore path: roles are not in a Coolify dump. Re-run the provisioning job (idempotent; it is
|
|
49
|
+
// given a password only for a home not recorded), then `pg_restore` each dump with
|
|
50
|
+
// `--no-owner --no-acl --role=<owner>`. Template apps create `<db>_rt` again on first boot.
|
|
51
|
+
/** The server's name in Coolify. */
|
|
52
|
+
export const orgPostgresServerName = (org) => `${org.slug}-postgres`;
|
|
53
|
+
/** The job app's name in Coolify. */
|
|
54
|
+
export const orgPostgresJobName = (org) => `${org.slug}-postgres-job`;
|
|
55
|
+
/** The job app's id while it exists. */
|
|
56
|
+
export const ORG_POSTGRES_JOB_APP_SCOPE_KEY = 'org_postgres_job_app';
|
|
57
|
+
/** An opaque id, set between a password's mint and its delivery. Never the password. */
|
|
58
|
+
export const orgPostgresUndeliveredScopeKey = (home) => `org_postgres_undelivered.${home}`;
|
|
59
|
+
/** Where the host holds a home's password, once delivered. */
|
|
60
|
+
export const orgPostgresSecretRef = (org, home, role) => `org-postgres:${org.slug}:${home}:${role}`;
|
|
61
|
+
/** How many times each password is handed to `deliverSecret`. */
|
|
62
|
+
export const ORG_POSTGRES_DELIVERY_ATTEMPTS = 3;
|
|
63
|
+
/**
|
|
64
|
+
* The two steps that make the company's Postgres server and its databases true, from recorded ids
|
|
65
|
+
* alone: the server while it is not recorded, the databases of every enabled home that is not. The
|
|
66
|
+
* databases step needs no precondition beyond the server step before it in the same plan.
|
|
67
|
+
*/
|
|
68
|
+
export function planOrgPostgresSteps(org, plan) {
|
|
69
|
+
const steps = [];
|
|
70
|
+
if (plan.serverMissing) {
|
|
71
|
+
steps.push({
|
|
72
|
+
action: { system: 'coolify', kind: 'org-postgres', key: org.id, desired: {} },
|
|
73
|
+
step: {
|
|
74
|
+
description: `create ${org.slug}'s one Postgres server (${orgPostgresServerName(org)})`,
|
|
75
|
+
destroys: false,
|
|
76
|
+
migrates: false,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (plan.missingSchemas.length > 0) {
|
|
81
|
+
const homes = plan.missingSchemas.map((home) => home.schema);
|
|
82
|
+
steps.push({
|
|
83
|
+
action: {
|
|
84
|
+
system: 'coolify',
|
|
85
|
+
kind: 'org-postgres-databases',
|
|
86
|
+
key: org.id,
|
|
87
|
+
desired: { homes },
|
|
88
|
+
},
|
|
89
|
+
step: {
|
|
90
|
+
description: `create ${org.slug}'s databases and roles on its Postgres server: ${homes.join(', ')}`,
|
|
91
|
+
destroys: false,
|
|
92
|
+
migrates: false,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return steps;
|
|
97
|
+
}
|
|
98
|
+
const mint = () => randomBytes(24).toString('base64url');
|
|
99
|
+
const mintOpaque = () => randomBytes(12).toString('hex');
|
|
100
|
+
/** Creates the company's Postgres server once and starts it. Returns the Coolify uuid. */
|
|
101
|
+
export async function provisionOrgPostgresServer(scope, deps, org) {
|
|
102
|
+
const coolify = withCallTimeout(deps.coolify);
|
|
103
|
+
let id = scope.recordedIds()[ORG_POSTGRES_SERVER_SCOPE_KEY];
|
|
104
|
+
if (!id) {
|
|
105
|
+
const projectUuid = await deps.ensureProject();
|
|
106
|
+
const { environment } = await coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
107
|
+
const name = orgPostgresServerName(org);
|
|
108
|
+
const found = (await coolify.listDatabases()).find((db) => db.name === name && db.environment_id === environment.id);
|
|
109
|
+
if (found) {
|
|
110
|
+
id = found.uuid;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const created = await coolify.createPostgresDatabase({
|
|
114
|
+
project_uuid: projectUuid,
|
|
115
|
+
server_uuid: await deps.serverUuid(),
|
|
116
|
+
environment_name: environment.name,
|
|
117
|
+
environment_uuid: environment.uuid,
|
|
118
|
+
name,
|
|
119
|
+
postgres_db: 'postgres',
|
|
120
|
+
postgres_user: 'postgres',
|
|
121
|
+
});
|
|
122
|
+
id = created.uuid;
|
|
123
|
+
}
|
|
124
|
+
// Recorded before it is started: a crash after the create must not make a second container.
|
|
125
|
+
scope.recordScopeId(ORG_POSTGRES_SERVER_SCOPE_KEY, id);
|
|
126
|
+
await scope.persist();
|
|
127
|
+
}
|
|
128
|
+
await boundedCall(deps.startDatabase(id), 'Coolify startDatabase');
|
|
129
|
+
return { resourceId: id };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Creates the databases and roles of the homes in `homeKeys` that are not recorded, with one job app
|
|
133
|
+
* for all of them, then hands each owner password to the host and records the home.
|
|
134
|
+
*
|
|
135
|
+
* One mint (CLAUDE.md platform rule 1): each home's password is minted once per attempt, an opaque id
|
|
136
|
+
* is recorded under `org_postgres_undelivered.<home>` and persisted before the job runs, and the home
|
|
137
|
+
* is recorded only after the password is delivered. A run that dies between the two finds the id,
|
|
138
|
+
* and mints again: no consumer has read the old password, and the job sets the new one.
|
|
139
|
+
*/
|
|
140
|
+
export async function provisionOrgPostgresDatabases(scope, deps, org, homeKeys) {
|
|
141
|
+
const label = `${org.slug}'s Postgres databases`;
|
|
142
|
+
const coolify = withCallTimeout(deps.coolify);
|
|
143
|
+
const recorded = scope.recordedIds();
|
|
144
|
+
const serverId = recorded[ORG_POSTGRES_SERVER_SCOPE_KEY];
|
|
145
|
+
if (!serverId)
|
|
146
|
+
throw new Error(`${label}: the Postgres server is not recorded yet`);
|
|
147
|
+
const homes = ORG_SERVICE_SCHEMAS.filter((home) => homeKeys.includes(home.schema) && !recorded[home.scopeKey]);
|
|
148
|
+
if (homes.length === 0)
|
|
149
|
+
return;
|
|
150
|
+
const databases = homes.map((home) => orgServiceDatabase(org, home));
|
|
151
|
+
const onSecret = (value) => deps.onSecret?.(value);
|
|
152
|
+
const server = await boundedCall(deps.startDatabase(serverId), 'Coolify startDatabase');
|
|
153
|
+
if (!server.internal_db_url)
|
|
154
|
+
throw new Error(`${label}: the Postgres server has no internal URL`);
|
|
155
|
+
const adminUrl = server.internal_db_url;
|
|
156
|
+
onSecret(adminUrl);
|
|
157
|
+
if (URL.canParse(adminUrl)) {
|
|
158
|
+
const password = decodeURIComponent(new URL(adminUrl).password);
|
|
159
|
+
if (password)
|
|
160
|
+
onSecret(password);
|
|
161
|
+
}
|
|
162
|
+
const mintPassword = deps.mintPassword ?? mint;
|
|
163
|
+
const passwords = new Map();
|
|
164
|
+
for (const home of databases) {
|
|
165
|
+
const owner = mintPassword();
|
|
166
|
+
const runtime = jobCreatesRuntimeRole(home) ? mintPassword() : null;
|
|
167
|
+
onSecret(owner);
|
|
168
|
+
if (runtime)
|
|
169
|
+
onSecret(runtime);
|
|
170
|
+
passwords.set(home.schema, { owner, runtime });
|
|
171
|
+
scope.recordScopeId(orgPostgresUndeliveredScopeKey(home.schema), (deps.mintId ?? mintOpaque)());
|
|
172
|
+
}
|
|
173
|
+
await scope.persist();
|
|
174
|
+
await runJob(scope, deps, org, label, coolify, databases, adminUrl, passwords);
|
|
175
|
+
const failed = [];
|
|
176
|
+
for (const home of databases) {
|
|
177
|
+
const held = passwords.get(home.schema);
|
|
178
|
+
if (!held)
|
|
179
|
+
continue;
|
|
180
|
+
try {
|
|
181
|
+
await deliver(deps, orgPostgresSecretRef(org, home.schema, 'owner'), held.owner);
|
|
182
|
+
if (held.runtime) {
|
|
183
|
+
await deliver(deps, orgPostgresSecretRef(org, home.schema, 'runtime'), held.runtime);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
failed.push(home.schema);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
scope.recordScopeId(`org_postgres_schema.${home.schema}`, home.database);
|
|
191
|
+
scope.recordScopeId(orgPostgresUndeliveredScopeKey(home.schema), '');
|
|
192
|
+
await scope.persist();
|
|
193
|
+
}
|
|
194
|
+
if (failed.length > 0) {
|
|
195
|
+
throw new Error(`${label}: the passwords for ${failed.join(', ')} were set but could not be delivered after ${ORG_POSTGRES_DELIVERY_ATTEMPTS} attempts; the next apply sets and delivers new ones`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function deliver(deps, ref, secret) {
|
|
199
|
+
for (let attempt = 1; attempt <= ORG_POSTGRES_DELIVERY_ATTEMPTS; attempt += 1) {
|
|
200
|
+
try {
|
|
201
|
+
await boundedCall(deps.deliverSecret(ref, secret), 'deliverSecret');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// The cause may carry the secret; nothing of it is kept.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new Error('delivery failed');
|
|
209
|
+
}
|
|
210
|
+
async function runJob(scope, deps, org, label, coolify, databases, adminUrl, passwords) {
|
|
211
|
+
const jobName = orgPostgresJobName(org);
|
|
212
|
+
let jobAppId;
|
|
213
|
+
try {
|
|
214
|
+
const projectUuid = await deps.ensureProject();
|
|
215
|
+
const { environment } = await coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
216
|
+
// A job left by an earlier run holds an old script and old passwords: it is replaced, never reused.
|
|
217
|
+
const leftover = await coolify.findApplicationByName(jobName, {
|
|
218
|
+
environmentIds: [environment.id],
|
|
219
|
+
});
|
|
220
|
+
if (leftover)
|
|
221
|
+
await coolify.raw('DELETE', `/applications/${leftover.uuid}`);
|
|
222
|
+
const created = await coolify.createDockerfileApplication({
|
|
223
|
+
project_uuid: projectUuid,
|
|
224
|
+
server_uuid: await deps.serverUuid(),
|
|
225
|
+
environment_name: environment.name,
|
|
226
|
+
environment_uuid: environment.uuid,
|
|
227
|
+
dockerfile: orgPostgresProvisionDockerfile(databases),
|
|
228
|
+
name: jobName,
|
|
229
|
+
description: `${org.slug}'s Postgres databases job`,
|
|
230
|
+
instant_deploy: false,
|
|
231
|
+
autogenerate_domain: false,
|
|
232
|
+
});
|
|
233
|
+
jobAppId = created.uuid;
|
|
234
|
+
scope.recordScopeId(ORG_POSTGRES_JOB_APP_SCOPE_KEY, jobAppId);
|
|
235
|
+
await scope.persist();
|
|
236
|
+
await coolify.updateApplication(jobAppId, { health_check_enabled: false });
|
|
237
|
+
await coolify.bulkUpsertEnvs(jobAppId, [
|
|
238
|
+
{ key: ORG_POSTGRES_JOB_ADMIN_ENV, value: adminUrl },
|
|
239
|
+
...databases.flatMap((home) => {
|
|
240
|
+
const held = passwords.get(home.schema);
|
|
241
|
+
if (!held)
|
|
242
|
+
return [];
|
|
243
|
+
return [
|
|
244
|
+
{ key: orgPostgresPasswordEnv(home.schema), value: held.owner },
|
|
245
|
+
...(held.runtime
|
|
246
|
+
? [{ key: orgPostgresRuntimePasswordEnv(home.schema), value: held.runtime }]
|
|
247
|
+
: []),
|
|
248
|
+
];
|
|
249
|
+
}),
|
|
250
|
+
]);
|
|
251
|
+
await coolify.deploy(jobAppId, true);
|
|
252
|
+
await waitForJob(coolify, jobAppId, deps.runtime ?? {}, {
|
|
253
|
+
job: 'the Postgres job',
|
|
254
|
+
stopped: 'the Postgres job stopped before it finished',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const why = error instanceof JobFailure ? error.message : 'a Coolify call failed or timed out';
|
|
259
|
+
// The failed job app must not keep running with the admin URL and passwords in its env.
|
|
260
|
+
let cleaned = '';
|
|
261
|
+
if (jobAppId) {
|
|
262
|
+
const id = jobAppId;
|
|
263
|
+
const blank = [
|
|
264
|
+
ORG_POSTGRES_JOB_ADMIN_ENV,
|
|
265
|
+
...databases.flatMap((home) => [
|
|
266
|
+
orgPostgresPasswordEnv(home.schema),
|
|
267
|
+
orgPostgresRuntimePasswordEnv(home.schema),
|
|
268
|
+
]),
|
|
269
|
+
].map((key) => ({ key, value: '' }));
|
|
270
|
+
try {
|
|
271
|
+
await boundedCall(coolify.raw('POST', `/applications/${id}/stop`), 'stop the job');
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
cleaned += '; the job app could not be stopped, stop it in Coolify';
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
await boundedCall(coolify.bulkUpsertEnvs(id, blank), 'clear the job env');
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
cleaned +=
|
|
281
|
+
'; its env could not be cleared, delete the app in Coolify (it holds the admin URL)';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw new Error(`${label}: ${why}${jobAppId ? `; the job app ${jobAppId} is left stopped without secrets for inspection and the next apply replaces it${cleaned}` : ''}; apply again`);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
await coolify.raw('DELETE', `/applications/${jobAppId}`);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
throw new Error(`${label}: the job ran, but its app ${jobAppId} could not be deleted; delete it in Coolify (it holds the admin URL), then apply again`);
|
|
291
|
+
}
|
|
292
|
+
scope.recordScopeId(ORG_POSTGRES_JOB_APP_SCOPE_KEY, '');
|
|
293
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DestroyProgress, PlanApply } from '../apply.js';
|
|
2
|
-
import type { DeclaredOrg, OrgId } from '../org.js';
|
|
2
|
+
import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
|
|
3
3
|
import type { Plan, PlanStep } from '../plan.js';
|
|
4
4
|
import type { DeclaredPlatform } from '../platform.js';
|
|
5
5
|
import type { ReleaseStore } from '../release/state.js';
|
|
@@ -14,6 +14,7 @@ import { type UnconfirmedUpgrade } from './confirm-upgrade.js';
|
|
|
14
14
|
import { type StepAction } from './diff.js';
|
|
15
15
|
import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
|
|
16
16
|
import { type LoginHostDiscoveryPort } from './login-host.js';
|
|
17
|
+
import { type OrgPostgresMigrationConfirmation } from './org-postgres-migrate.js';
|
|
17
18
|
import { type EdgePortName, type HeldKeyResolver, type PlatformOwnerInput } from './platform-org.js';
|
|
18
19
|
import type { CallsSfuConnectionPort } from './platform-sfu.js';
|
|
19
20
|
import { type ReleaseContext, type ReleaseHost } from './release-host.js';
|
|
@@ -62,9 +63,9 @@ export interface AuditLog {
|
|
|
62
63
|
recordAdoption(entry: {
|
|
63
64
|
actor: AuditActor;
|
|
64
65
|
orgId: OrgId;
|
|
65
|
-
action: 'adopt' | 'release';
|
|
66
|
+
action: 'adopt' | 'release' | 'confirm-org-postgres-migration';
|
|
66
67
|
entryKey: string;
|
|
67
|
-
outcome: 'adopted' | 'released' | 'refused';
|
|
68
|
+
outcome: 'adopted' | 'released' | 'refused' | 'recorded';
|
|
68
69
|
reason?: string;
|
|
69
70
|
at: Date;
|
|
70
71
|
}): Promise<void>;
|
|
@@ -281,6 +282,8 @@ export interface PlatformStepExecutionContext {
|
|
|
281
282
|
export interface PlatformDriftedSteps {
|
|
282
283
|
archon?: boolean;
|
|
283
284
|
auth?: boolean;
|
|
285
|
+
/** #643: a recorded sign-in app (ZITADEL, login or broker) is gone from Coolify or not running. */
|
|
286
|
+
authApps?: boolean;
|
|
284
287
|
/** The host sets it. */
|
|
285
288
|
edge?: boolean;
|
|
286
289
|
/** #380 */
|
|
@@ -440,6 +443,18 @@ export declare class PlatformConfirmationRequiredError extends Error {
|
|
|
440
443
|
export interface FoundryApplyOptions {
|
|
441
444
|
/** Service names (`VersionChangeItem.service`), matched exactly, whose version change the operator confirmed. */
|
|
442
445
|
confirmUpgrade?: readonly string[];
|
|
446
|
+
/**
|
|
447
|
+
* A company's own confirmation for one of its apps, in place of the org's name. `apply` passes a
|
|
448
|
+
* destroying or migrating step only when {@link stepEntryKey} says it belongs to `entryKey` and
|
|
449
|
+
* `typed` equals `entryKey` exactly. Every other such step still needs the org's name, as does
|
|
450
|
+
* the whole plan when `typed` differs. It never passes a platform step.
|
|
451
|
+
*/
|
|
452
|
+
entryConfirmation?: {
|
|
453
|
+
entryKey: string;
|
|
454
|
+
typed: string;
|
|
455
|
+
/** The removed entry's gate keys, read by the caller from the company before the change. */
|
|
456
|
+
gates?: readonly string[];
|
|
457
|
+
};
|
|
443
458
|
}
|
|
444
459
|
export interface FoundryPlanApplyOptions {
|
|
445
460
|
store: DeclaredOrgStore;
|
|
@@ -466,6 +481,12 @@ export interface FoundryPlanApplyOptions {
|
|
|
466
481
|
* before zitadel-login's binding can be written.
|
|
467
482
|
*/
|
|
468
483
|
loginHostBindings?: boolean;
|
|
484
|
+
/**
|
|
485
|
+
* True when the host's `loginHostBindings` port writes the `clientOrganizations` map
|
|
486
|
+
* (`upsertClientOrganizations` and `deleteClientOrganizations`). Absent or false: the plan says in one
|
|
487
|
+
* fixed line that the shared login does not check customers clients' organisation.
|
|
488
|
+
*/
|
|
489
|
+
loginHostClientOrganizations?: boolean;
|
|
469
490
|
/**
|
|
470
491
|
* Reads `GET https://<login host>/.well-known/openid-configuration` for a
|
|
471
492
|
* company's login host (5 s limit). Optional, and without it every company's
|
|
@@ -481,6 +502,15 @@ export interface FoundryPlanApplyOptions {
|
|
|
481
502
|
* token of its existing sign-in apps.
|
|
482
503
|
*/
|
|
483
504
|
zitadelIdTokenUserinfo?: boolean;
|
|
505
|
+
/** True when the host's ZITADEL client has `updateOidcAppPostLogoutRedirectUris`; without it no org plans the post-logout address update. */
|
|
506
|
+
zitadelPostLogoutUris?: boolean;
|
|
507
|
+
/**
|
|
508
|
+
* True when the host's ZITADEL client has `getOidcAppRedirectUris` and
|
|
509
|
+
* `updateOidcAppRedirectUris` (`@wtfalch/providers` 0.12.0). Optional, and
|
|
510
|
+
* false without them: no org plans the step that registers the desktop
|
|
511
|
+
* sign-in callback on its existing sign-in apps.
|
|
512
|
+
*/
|
|
513
|
+
zitadelRedirectUris?: boolean;
|
|
484
514
|
/**
|
|
485
515
|
* Auth ADR 0002: true when the host's ZITADEL client has `ensureOidcAppMcpAccess`
|
|
486
516
|
* and `ensureAccessTokenLifetime` (`@wtfalch/providers` 0.11.0). Without it every
|
|
@@ -644,8 +674,11 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
644
674
|
private readonly agora;
|
|
645
675
|
private readonly jobsScheduler;
|
|
646
676
|
private readonly loginHostBindings;
|
|
677
|
+
private readonly loginHostClientOrganizations;
|
|
647
678
|
private readonly loginHostDiscovery;
|
|
648
679
|
private readonly zitadelIdTokenUserinfo;
|
|
680
|
+
private readonly zitadelPostLogoutUris;
|
|
681
|
+
private readonly zitadelRedirectUris;
|
|
649
682
|
private readonly zitadelMcp;
|
|
650
683
|
private readonly zitadelOrgMetadata;
|
|
651
684
|
private readonly broker;
|
|
@@ -706,6 +739,13 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
706
739
|
* {@link apply}. Every path writes an audit row.
|
|
707
740
|
*/
|
|
708
741
|
release(id: OrgId, entryKey: string): Promise<AppliedState>;
|
|
742
|
+
/**
|
|
743
|
+
* Records that the staging copy of a live company's data was checked, which is the only thing that lets
|
|
744
|
+
* the org Postgres migration step run (#444). It is its own call, never part of an apply. Needs the
|
|
745
|
+
* platform permission. It stores the staging copy id, who confirmed and when, and writes an audit row.
|
|
746
|
+
* Throws {@link InvalidMigrationConfirmationError} for a confirmation that is not valid.
|
|
747
|
+
*/
|
|
748
|
+
confirmOrgPostgresMigration(orgId: OrgId, confirmation: OrgPostgresMigrationConfirmation): Promise<void>;
|
|
709
749
|
private computeAdopt;
|
|
710
750
|
apply(plan: Plan, confirmedOrgName: string, options?: FoundryApplyOptions): Promise<AppliedState>;
|
|
711
751
|
/**
|
|
@@ -919,3 +959,20 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
919
959
|
* than a second copy that could drift from it.
|
|
920
960
|
*/
|
|
921
961
|
export declare function planNeedsConfirmation(steps: readonly PlanStep[]): boolean;
|
|
962
|
+
/**
|
|
963
|
+
* The key of the hosting entry a step belongs to, or `null` for a step of the org or the platform.
|
|
964
|
+
* An entry's own steps are keyed `<orgId>:<entry key>` (its Coolify app, Worker, Worker app, and
|
|
965
|
+
* ai, edge and files keys); with `orgId` the prefix must be exactly that id. With `entries` (the
|
|
966
|
+
* declared ones) two more are known: the ZITADEL sign-in client named like an entry that declares
|
|
967
|
+
* `uses.auth`, and a gate named in an entry's `gates`. The org database, the org-wide files key,
|
|
968
|
+
* the customers organisation and the issuer belong to no entry. (`apply` lets an entry's
|
|
969
|
+
* confirmation pass the park of the org database once no declared entry uses a database.)
|
|
970
|
+
*/
|
|
971
|
+
export declare function stepEntryKey(step: {
|
|
972
|
+
action: Pick<StepAction, 'system' | 'kind' | 'key'>;
|
|
973
|
+
}, orgId?: string, entries?: readonly Pick<HostingEntry, 'key' | 'uses' | 'gates'>[]): string | null;
|
|
974
|
+
/** Whether `confirmation` passes every destroying or migrating step of `steps`: all belong to its entry, and it typed the key. */
|
|
975
|
+
export declare function entryConfirms(confirmation: FoundryApplyOptions['entryConfirmation'], steps: readonly {
|
|
976
|
+
step: PlanStep;
|
|
977
|
+
action: StepAction;
|
|
978
|
+
}[], orgId: string, entries: readonly HostingEntry[]): boolean;
|