@wtfalch/foundry 0.45.1 → 0.46.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/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/org.d.ts +37 -0
- package/dist/org.js +135 -10
- 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 +272 -84
- package/dist/plan/executor.d.ts +136 -6
- package/dist/plan/executor.js +725 -98
- package/dist/plan/health-gaps.js +5 -0
- package/dist/plan/index.d.ts +3 -0
- package/dist/plan/index.js +2 -0
- 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 +30 -2
- package/dist/plan/plan-apply.js +104 -7
- 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 +279 -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 +41 -1
- 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/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
|
+
}
|
|
@@ -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 */
|
|
@@ -466,6 +469,12 @@ export interface FoundryPlanApplyOptions {
|
|
|
466
469
|
* before zitadel-login's binding can be written.
|
|
467
470
|
*/
|
|
468
471
|
loginHostBindings?: boolean;
|
|
472
|
+
/**
|
|
473
|
+
* True when the host's `loginHostBindings` port writes the `clientOrganizations` map
|
|
474
|
+
* (`upsertClientOrganizations` and `deleteClientOrganizations`). Absent or false: the plan says in one
|
|
475
|
+
* fixed line that the shared login does not check customers clients' organisation.
|
|
476
|
+
*/
|
|
477
|
+
loginHostClientOrganizations?: boolean;
|
|
469
478
|
/**
|
|
470
479
|
* Reads `GET https://<login host>/.well-known/openid-configuration` for a
|
|
471
480
|
* company's login host (5 s limit). Optional, and without it every company's
|
|
@@ -481,6 +490,15 @@ export interface FoundryPlanApplyOptions {
|
|
|
481
490
|
* token of its existing sign-in apps.
|
|
482
491
|
*/
|
|
483
492
|
zitadelIdTokenUserinfo?: boolean;
|
|
493
|
+
/** True when the host's ZITADEL client has `updateOidcAppPostLogoutRedirectUris`; without it no org plans the post-logout address update. */
|
|
494
|
+
zitadelPostLogoutUris?: boolean;
|
|
495
|
+
/**
|
|
496
|
+
* True when the host's ZITADEL client has `getOidcAppRedirectUris` and
|
|
497
|
+
* `updateOidcAppRedirectUris` (`@wtfalch/providers` 0.12.0). Optional, and
|
|
498
|
+
* false without them: no org plans the step that registers the desktop
|
|
499
|
+
* sign-in callback on its existing sign-in apps.
|
|
500
|
+
*/
|
|
501
|
+
zitadelRedirectUris?: boolean;
|
|
484
502
|
/**
|
|
485
503
|
* Auth ADR 0002: true when the host's ZITADEL client has `ensureOidcAppMcpAccess`
|
|
486
504
|
* and `ensureAccessTokenLifetime` (`@wtfalch/providers` 0.11.0). Without it every
|
|
@@ -644,8 +662,11 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
644
662
|
private readonly agora;
|
|
645
663
|
private readonly jobsScheduler;
|
|
646
664
|
private readonly loginHostBindings;
|
|
665
|
+
private readonly loginHostClientOrganizations;
|
|
647
666
|
private readonly loginHostDiscovery;
|
|
648
667
|
private readonly zitadelIdTokenUserinfo;
|
|
668
|
+
private readonly zitadelPostLogoutUris;
|
|
669
|
+
private readonly zitadelRedirectUris;
|
|
649
670
|
private readonly zitadelMcp;
|
|
650
671
|
private readonly zitadelOrgMetadata;
|
|
651
672
|
private readonly broker;
|
|
@@ -706,6 +727,13 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
706
727
|
* {@link apply}. Every path writes an audit row.
|
|
707
728
|
*/
|
|
708
729
|
release(id: OrgId, entryKey: string): Promise<AppliedState>;
|
|
730
|
+
/**
|
|
731
|
+
* Records that the staging copy of a live company's data was checked, which is the only thing that lets
|
|
732
|
+
* the org Postgres migration step run (#444). It is its own call, never part of an apply. Needs the
|
|
733
|
+
* platform permission. It stores the staging copy id, who confirmed and when, and writes an audit row.
|
|
734
|
+
* Throws {@link InvalidMigrationConfirmationError} for a confirmation that is not valid.
|
|
735
|
+
*/
|
|
736
|
+
confirmOrgPostgresMigration(orgId: OrgId, confirmation: OrgPostgresMigrationConfirmation): Promise<void>;
|
|
709
737
|
private computeAdopt;
|
|
710
738
|
apply(plan: Plan, confirmedOrgName: string, options?: FoundryApplyOptions): Promise<AppliedState>;
|
|
711
739
|
/**
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
3
3
|
import { sfuSlotPorts } from '../calls-sfu.js';
|
|
4
|
-
import { heldEnvRef, ownsServices, validateHosting } from '../org.js';
|
|
4
|
+
import { ORG_SERVICE_SCHEMAS, heldEnvRef, ownsServices, validateHosting } from '../org.js';
|
|
5
5
|
import { PLATFORM_SERVICE_KEYS } from '../platform.js';
|
|
6
6
|
import { AlreadyRunningError, StepFailedError } from '../runner/errors.js';
|
|
7
7
|
import { StaleLeaseError, } from '../store.js';
|
|
8
8
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
9
9
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
10
|
-
import { NetworkMoveBackupMissingError, SigninDbMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupForSigninMove, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
10
|
+
import { NetworkMoveBackupMissingError, OrgPostgresMigrationBackupMissingError, SigninDbMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupForOrgPostgresMigration, requireFreshBackupForSigninMove, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
11
11
|
import { ARCHON_REPO, PRIMARY_SERVER_SCOPE_KEY, archonSubject, entrySubject, } from './blue-green.js';
|
|
12
12
|
import { readLiveBuilds } from './build-record.js';
|
|
13
|
+
import { LEGACY_BACKUP_NAMES } from './company-backups.js';
|
|
13
14
|
import { unconfirmedUpgrades } from './confirm-upgrade.js';
|
|
14
15
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
15
16
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
@@ -20,6 +21,7 @@ import { hostingNames } from './locked-hostnames.js';
|
|
|
20
21
|
import { agoraIssuerHostFor, loginHostServesAgora, readLoginHostDiscovery, } from './login-host.js';
|
|
21
22
|
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';
|
|
22
23
|
import { findNameClashes, nameClashMessage } from './org-names.js';
|
|
24
|
+
import { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationNotConfirmedError, invalidMigrationConfirmation, migrateHomeOf, parseMigrationConfirmation, } from './org-postgres-migrate.js';
|
|
23
25
|
import { ownHostnames } from './own-hostnames.js';
|
|
24
26
|
import { OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, orgTarget } from './own-services.js';
|
|
25
27
|
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';
|
|
@@ -159,8 +161,11 @@ export class FoundryPlanApply {
|
|
|
159
161
|
agora;
|
|
160
162
|
jobsScheduler;
|
|
161
163
|
loginHostBindings;
|
|
164
|
+
loginHostClientOrganizations;
|
|
162
165
|
loginHostDiscovery;
|
|
163
166
|
zitadelIdTokenUserinfo;
|
|
167
|
+
zitadelPostLogoutUris;
|
|
168
|
+
zitadelRedirectUris;
|
|
164
169
|
zitadelMcp;
|
|
165
170
|
zitadelOrgMetadata;
|
|
166
171
|
broker;
|
|
@@ -194,8 +199,11 @@ export class FoundryPlanApply {
|
|
|
194
199
|
this.agora = options.agora;
|
|
195
200
|
this.jobsScheduler = options.jobsScheduler ?? false;
|
|
196
201
|
this.loginHostBindings = options.loginHostBindings ?? false;
|
|
202
|
+
this.loginHostClientOrganizations = options.loginHostClientOrganizations ?? false;
|
|
197
203
|
this.loginHostDiscovery = options.loginHostDiscovery;
|
|
198
204
|
this.zitadelIdTokenUserinfo = options.zitadelIdTokenUserinfo ?? false;
|
|
205
|
+
this.zitadelPostLogoutUris = options.zitadelPostLogoutUris ?? false;
|
|
206
|
+
this.zitadelRedirectUris = options.zitadelRedirectUris ?? false;
|
|
199
207
|
this.zitadelMcp = options.zitadelMcp ?? false;
|
|
200
208
|
this.zitadelOrgMetadata = options.zitadelOrgMetadata ?? false;
|
|
201
209
|
this.broker = options.broker ?? false;
|
|
@@ -379,6 +387,55 @@ export class FoundryPlanApply {
|
|
|
379
387
|
return released;
|
|
380
388
|
}));
|
|
381
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Records that the staging copy of a live company's data was checked, which is the only thing that lets
|
|
392
|
+
* the org Postgres migration step run (#444). It is its own call, never part of an apply. Needs the
|
|
393
|
+
* platform permission. It stores the staging copy id, who confirmed and when, and writes an audit row.
|
|
394
|
+
* Throws {@link InvalidMigrationConfirmationError} for a confirmation that is not valid.
|
|
395
|
+
*/
|
|
396
|
+
async confirmOrgPostgresMigration(orgId, confirmation) {
|
|
397
|
+
const record = (outcome, reason) => this.audit.recordAdoption({
|
|
398
|
+
actor: this.actor,
|
|
399
|
+
orgId,
|
|
400
|
+
action: 'confirm-org-postgres-migration',
|
|
401
|
+
entryKey: 'org_postgres_migration',
|
|
402
|
+
outcome,
|
|
403
|
+
...(reason === undefined ? {} : { reason }),
|
|
404
|
+
at: this.now(),
|
|
405
|
+
});
|
|
406
|
+
if ((await this.authz.canApplyPlatform?.(this.actor)) !== true) {
|
|
407
|
+
await record('refused', 'not authorized');
|
|
408
|
+
throw new NotAuthorizedError(this.actor, orgId);
|
|
409
|
+
}
|
|
410
|
+
const invalid = invalidMigrationConfirmation(confirmation);
|
|
411
|
+
if (invalid) {
|
|
412
|
+
await record('refused', invalid);
|
|
413
|
+
throw new InvalidMigrationConfirmationError(invalid);
|
|
414
|
+
}
|
|
415
|
+
const value = {
|
|
416
|
+
stagingCopy: confirmation.stagingCopy,
|
|
417
|
+
confirmedBy: confirmation.confirmedBy,
|
|
418
|
+
confirmedAt: confirmation.confirmedAt,
|
|
419
|
+
};
|
|
420
|
+
await this.withMutationLease(companyScope(orgId), (reason) => record('refused', reason), (fence) => this.withRunnerSerialization(orgId, (reason) => record('refused', reason), async () => {
|
|
421
|
+
if (!(await this.store.get(orgId))) {
|
|
422
|
+
await record('refused', `no declared org ${orgId}`);
|
|
423
|
+
throw new DeclaredOrgNotFoundError(orgId);
|
|
424
|
+
}
|
|
425
|
+
const previous = await this.store.getAppliedState(orgId);
|
|
426
|
+
await this.store.putAppliedState({
|
|
427
|
+
orgId,
|
|
428
|
+
serviceIds: {
|
|
429
|
+
...(previous?.serviceIds ?? {}),
|
|
430
|
+
[ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY]: JSON.stringify(value),
|
|
431
|
+
},
|
|
432
|
+
checkpoints: previous?.checkpoints ?? [],
|
|
433
|
+
failedFields: previous?.failedFields ?? [],
|
|
434
|
+
lastApplied: previous?.lastApplied ?? null,
|
|
435
|
+
}, fence);
|
|
436
|
+
await record('recorded');
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
382
439
|
async computeAdopt(id, input) {
|
|
383
440
|
const org = await this.store.get(id);
|
|
384
441
|
if (!org)
|
|
@@ -485,6 +542,40 @@ export class FoundryPlanApply {
|
|
|
485
542
|
throw e;
|
|
486
543
|
}
|
|
487
544
|
}
|
|
545
|
+
// A migration to the company's Postgres server needs the recorded staging confirmation and a fresh dump
|
|
546
|
+
// of the old database. Both are checked before the first port call.
|
|
547
|
+
const migrations = detail.plannedSteps.filter((s) => s.action.system === 'coolify' && s.action.kind === 'org-postgres-migrate');
|
|
548
|
+
if (migrations.length > 0) {
|
|
549
|
+
const ids = (await this.store.getAppliedState(orgId))?.serviceIds ?? {};
|
|
550
|
+
if (!parseMigrationConfirmation(ids[ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY])) {
|
|
551
|
+
await this.refuse(plan, 'org postgres migration without a recorded staging confirmation');
|
|
552
|
+
throw new OrgPostgresMigrationNotConfirmedError();
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
for (const migration of migrations) {
|
|
556
|
+
const home = ORG_SERVICE_SCHEMAS.find((h) => h.schema === migrateHomeOf(migration.action));
|
|
557
|
+
const legacyKey = home?.legacyKey(org);
|
|
558
|
+
const backupName = home ? LEGACY_BACKUP_NAMES[home.schema] : undefined;
|
|
559
|
+
if (!this.backupGate)
|
|
560
|
+
throw new OrgPostgresMigrationBackupMissingError('no-gate');
|
|
561
|
+
if (!legacyKey || !backupName) {
|
|
562
|
+
throw new OrgPostgresMigrationBackupMissingError('no-backup');
|
|
563
|
+
}
|
|
564
|
+
await requireFreshBackupForOrgPostgresMigration({
|
|
565
|
+
coolify: this.backupGate.coolify,
|
|
566
|
+
orgId,
|
|
567
|
+
recordedIds: ids,
|
|
568
|
+
now: () => this.now().getTime(),
|
|
569
|
+
}, legacyKey, backupName);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
if (e instanceof OrgPostgresMigrationBackupMissingError) {
|
|
574
|
+
await this.refuse(plan, 'org postgres migration without a fresh backup');
|
|
575
|
+
}
|
|
576
|
+
throw e;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
488
579
|
if (planNeedsConfirmation(plan.steps) &&
|
|
489
580
|
confirmedOrgName.trim() !== org.displayName.trim()) {
|
|
490
581
|
await this.refuse(plan, "destructive or migrating step without the org's name typed correctly");
|
|
@@ -1219,8 +1310,11 @@ export class FoundryPlanApply {
|
|
|
1219
1310
|
: undefined;
|
|
1220
1311
|
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, {
|
|
1221
1312
|
loginHostBindings: this.loginHostBindings,
|
|
1313
|
+
loginHostClientOrganizations: this.loginHostClientOrganizations,
|
|
1222
1314
|
loginHostServesDiscovery,
|
|
1223
1315
|
zitadelIdTokenUserinfo: this.zitadelIdTokenUserinfo,
|
|
1316
|
+
zitadelPostLogoutUris: this.zitadelPostLogoutUris,
|
|
1317
|
+
zitadelRedirectUris: this.zitadelRedirectUris,
|
|
1224
1318
|
zitadelMcp: this.zitadelMcp,
|
|
1225
1319
|
zitadelOrgMetadata: this.zitadelOrgMetadata,
|
|
1226
1320
|
broker: ownsServices(org) && this.hostOwnServices && this.executor.ownBrokerPort
|
|
@@ -1356,14 +1450,16 @@ export class FoundryPlanApply {
|
|
|
1356
1450
|
...(await this.sharedPlatformReadiness()),
|
|
1357
1451
|
heldEnv: await this.heldEnvReadiness(org),
|
|
1358
1452
|
};
|
|
1359
|
-
if (!ownsServices(org))
|
|
1360
|
-
return base;
|
|
1361
|
-
const held = this.platformDeps?.heldKeys;
|
|
1362
1453
|
const platform = await this.platformDeps?.store.get();
|
|
1363
|
-
|
|
1364
|
-
|
|
1454
|
+
// The auth release gates the shared-host organisation check and the cookie domain of every company.
|
|
1455
|
+
const withRefs = (readiness) => platform
|
|
1365
1456
|
? { ...readiness, serviceGitRefs: serviceGitRefsOf(platform, org.versionPin) }
|
|
1366
1457
|
: readiness;
|
|
1458
|
+
if (!ownsServices(org))
|
|
1459
|
+
return withRefs(base);
|
|
1460
|
+
const held = this.platformDeps?.heldKeys;
|
|
1461
|
+
const readiness = orgPlatformReadinessFor(org, base, orgRecordedIds, held ? await heldOwnKeyRefs(held, org) : new Set());
|
|
1462
|
+
return withRefs(readiness);
|
|
1367
1463
|
}
|
|
1368
1464
|
/**
|
|
1369
1465
|
* The org's `heldSecret` envs: the refs that hold a value now, which of those differ from the live
|
|
@@ -2022,6 +2118,7 @@ const STEP_FIELDS = {
|
|
|
2022
2118
|
'zitadel:owner': 'owners',
|
|
2023
2119
|
'zitadel:customers': 'hosting.entries.uses.auth',
|
|
2024
2120
|
'zitadel:id-token-userinfo': 'hosting.entries.uses.auth',
|
|
2121
|
+
'zitadel:desktop-callback': 'hosting.entries.uses.auth',
|
|
2025
2122
|
'zitadel:mcp-project': 'hosting.entries.uses.auth.mcp',
|
|
2026
2123
|
'zitadel:mcp-access': 'hosting.entries.uses.auth.mcp',
|
|
2027
2124
|
'zitadel:access-token-lifetime': 'hosting.entries.uses.auth.mcp',
|
|
@@ -60,6 +60,12 @@ export declare const PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY = "auth_z
|
|
|
60
60
|
export declare const AUTH_ZITADEL_RUN_OPTIONS = "--entrypoint \"/app/zitadel start-from-init --masterkeyFromEnv --tlsMode external\"";
|
|
61
61
|
/** The run options last applied to the ZITADEL app (value: the options string), so a changed value forces one redeploy. */
|
|
62
62
|
export declare const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = "zitadel_platform_auth_run_options_applied";
|
|
63
|
+
/**
|
|
64
|
+
* The `gitRef` the platform's login, in-app login and broker apps were last built from (value: the ref). A
|
|
65
|
+
* different declared ref plans the auth step again: it patches `git_branch` on those three apps and
|
|
66
|
+
* redeploys them, so a release tag named in the declaration reaches the running login.
|
|
67
|
+
*/
|
|
68
|
+
export declare const PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY = "platform_auth_applied_git_ref";
|
|
63
69
|
/**
|
|
64
70
|
* #251: the Synapse/MAS image tags an org's calls Matrix stack last
|
|
65
71
|
* successful apply actually set live. `coolifyCalls` (`executor.ts`)
|
|
@@ -611,6 +617,31 @@ export declare const PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY = "zitadel_platform_a
|
|
|
611
617
|
* repair while this differs from the recorded client app id.
|
|
612
618
|
*/
|
|
613
619
|
export declare const PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY = "archon_id_token_userinfo_applied";
|
|
620
|
+
/**
|
|
621
|
+
* The marker for Archon's sign-in client once its post-logout addresses were
|
|
622
|
+
* set: the client's ZITADEL app id and the set it holds
|
|
623
|
+
* (`postLogoutAppliedValue`). `planArchonPostLogoutUris` plans the update
|
|
624
|
+
* while this differs from what the declared hostname wants.
|
|
625
|
+
*/
|
|
626
|
+
export declare const PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY = "archon_post_logout_applied";
|
|
627
|
+
/**
|
|
628
|
+
* #394 (decision D2, release one): the role key on the platform's `mail`
|
|
629
|
+
* ZITADEL project that every mailbox user holds once the project's role check
|
|
630
|
+
* is on. This release only makes the role and grants it; the check stays off.
|
|
631
|
+
*/
|
|
632
|
+
export declare const PLATFORM_MAIL_USER_ROLE = "mail:use";
|
|
633
|
+
/** The mail project id once {@link PLATFORM_MAIL_USER_ROLE} exists on it. */
|
|
634
|
+
export declare const PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY = "zitadel_platform_mail_role";
|
|
635
|
+
/** `<mail project id>|<owner user id, or none>` once the owner holds the role. */
|
|
636
|
+
export declare const PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY = "zitadel_platform_mail_role_granted";
|
|
637
|
+
/** The mail project id once its role check and has-project check are on (release two, behind an option). */
|
|
638
|
+
export declare const PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY = "zitadel_platform_mail_role_check";
|
|
639
|
+
/**
|
|
640
|
+
* The ZITADEL app id of Archon's sign-in client once `/auth/desktop/callback`
|
|
641
|
+
* was registered on it (#339). `planArchonDesktopCallback` plans the step
|
|
642
|
+
* while this differs from the recorded client app id.
|
|
643
|
+
*/
|
|
644
|
+
export declare const PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY = "archon_desktop_callback_applied";
|
|
614
645
|
/**
|
|
615
646
|
* The settings `coolifyApp` owns on an entry's app (`folder`, `port`,
|
|
616
647
|
* `autoDeploy`) as the JSON the marker below holds. `coolifyApp` records it
|
|
@@ -1225,6 +1256,18 @@ export interface PlanPlatformOptions {
|
|
|
1225
1256
|
* repair step is not planned.
|
|
1226
1257
|
*/
|
|
1227
1258
|
archonIdTokenUserinfo?: boolean;
|
|
1259
|
+
/** The owner company's ZITADEL client has `updateOidcAppPostLogoutRedirectUris`. */
|
|
1260
|
+
archonPostLogoutUris?: boolean;
|
|
1261
|
+
/**
|
|
1262
|
+
* The Archon sign-in client's ZITADEL client has `getOidcAppRedirectUris`
|
|
1263
|
+
* and `updateOidcAppRedirectUris`. Not a block: without them the desktop
|
|
1264
|
+
* callback step is not planned.
|
|
1265
|
+
*/
|
|
1266
|
+
archonDesktopCallback?: boolean;
|
|
1267
|
+
/** The mail step's ZITADEL client can make a project role and grant it. Not a block: without it no mail role step is planned. */
|
|
1268
|
+
mailRole?: boolean;
|
|
1269
|
+
/** The host asked for the mail project's role check (release two) and its client has `setProjectRoleChecks`. */
|
|
1270
|
+
mailRoleCheck?: boolean;
|
|
1228
1271
|
/**
|
|
1229
1272
|
* The executor has the platform's sign-in dependencies and a ZITADEL
|
|
1230
1273
|
* client for its instance. Not a block: without it the sign-in policy
|
|
@@ -1265,6 +1308,8 @@ export interface PlanPlatformOptions {
|
|
|
1265
1308
|
drifted?: {
|
|
1266
1309
|
archon?: boolean;
|
|
1267
1310
|
auth?: boolean;
|
|
1311
|
+
/** #643: a recorded sign-in app is missing or not running; plans the step again, minting nothing. */
|
|
1312
|
+
authApps?: boolean;
|
|
1268
1313
|
edge?: boolean;
|
|
1269
1314
|
files?: boolean;
|
|
1270
1315
|
backupStorage?: boolean;
|