@wtfalch/foundry 0.37.0 → 0.39.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/bootstrap/versions.d.ts +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/plan/backup-gate.d.ts +19 -3
- package/dist/plan/backup-gate.js +49 -4
- package/dist/plan/company-backups.d.ts +7 -2
- package/dist/plan/company-backups.js +25 -6
- package/dist/plan/diff.d.ts +4 -0
- package/dist/plan/diff.js +19 -1
- package/dist/plan/executor.d.ts +13 -0
- package/dist/plan/executor.js +56 -13
- package/dist/plan/index.d.ts +2 -1
- package/dist/plan/index.js +1 -0
- package/dist/plan/mail-store.d.ts +34 -0
- package/dist/plan/mail-store.js +49 -0
- package/dist/plan/own-diff.d.ts +26 -0
- package/dist/plan/own-diff.js +74 -6
- package/dist/plan/own-services.d.ts +11 -0
- package/dist/plan/own-services.js +12 -0
- package/dist/plan/plan-apply.d.ts +4 -0
- package/dist/plan/plan-apply.js +30 -4
- package/dist/plan/platform-diff.d.ts +30 -0
- package/dist/plan/platform-diff.js +62 -7
- package/dist/plan/platform-org.d.ts +15 -0
- package/dist/plan/platform-org.js +17 -1
- package/dist/plan/platform-providers.d.ts +69 -2
- package/dist/plan/platform-providers.js +255 -41
- package/dist/plan/signin-db-backup.d.ts +105 -0
- package/dist/plan/signin-db-backup.js +193 -0
- package/dist/plan/signin-db-move.d.ts +68 -0
- package/dist/plan/signin-db-move.js +338 -0
- package/dist/plan/signin-db-tls.d.ts +69 -0
- package/dist/plan/signin-db-tls.js +255 -0
- package/dist/plan/signin-db.d.ts +114 -0
- package/dist/plan/signin-db.js +255 -0
- package/dist/plan/uptime-worker.d.ts +20 -1
- package/dist/plan/uptime-worker.js +121 -3
- package/package.json +1 -1
- package/versions.digests.json +2 -1
- package/versions.json +2 -1
package/dist/plan/own-diff.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface PlanOwnAuthOptions {
|
|
|
45
45
|
mailKeyHeld: boolean;
|
|
46
46
|
/** The host can mint that key itself (a Lettermint port and a held-key reader): the step plans without it. */
|
|
47
47
|
mailKeyMintable?: boolean;
|
|
48
|
+
/** The clock for the sign-in database certificate's renewal window (#746). Default `Date.now`. */
|
|
49
|
+
now?: number;
|
|
50
|
+
/** The held keys can store the sign-in database's CA and certificate. Default true. */
|
|
51
|
+
signinDbKeysPuttable?: boolean;
|
|
52
|
+
/** The host supplies the backup bucket the sign-in database dumps into. Default true. */
|
|
53
|
+
signinDbBackupConfigured?: boolean;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* An own org (`services.own`) runs its own sign-in: ZITADEL, the login app
|
|
@@ -59,6 +65,26 @@ export declare function planOwnAuth(options: PlanOwnAuthOptions): {
|
|
|
59
65
|
step: DraftStep | null;
|
|
60
66
|
blocked: string | null;
|
|
61
67
|
};
|
|
68
|
+
/** `unmanaged` key of the one line `planSigninDbMove` reports. */
|
|
69
|
+
export declare const OWN_SIGNIN_DB_MOVE_BLOCKED_KEY = "own-signin-db-move";
|
|
70
|
+
export interface PlanSigninDbMoveOptions {
|
|
71
|
+
org: DeclaredOrg;
|
|
72
|
+
recordedIds: Readonly<Record<string, string>>;
|
|
73
|
+
/** The held keys can store the sign-in database's CA and certificate. Default true. */
|
|
74
|
+
signinDbKeysPuttable?: boolean;
|
|
75
|
+
/** The host supplies the backup bucket the sign-in database dumps into. Default true. */
|
|
76
|
+
signinDbBackupConfigured?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One step for an own org whose sign-in is applied and whose ZITADEL still uses the Coolify database
|
|
80
|
+
* (#746): `signin-db-move.ts` copies it to foundry's TLS image. Never planned for a company on the TLS
|
|
81
|
+
* database, a new one, or one whose sign-in is not applied. The apply refuses it without a fresh
|
|
82
|
+
* backup of the old database (`backup-gate.ts`).
|
|
83
|
+
*/
|
|
84
|
+
export declare function planSigninDbMove(options: PlanSigninDbMoveOptions): {
|
|
85
|
+
step: DraftStep | null;
|
|
86
|
+
blocked: string | null;
|
|
87
|
+
};
|
|
62
88
|
/** `unmanaged` key of the one line `planOwnMail` reports. */
|
|
63
89
|
export declare const OWN_MAIL_BLOCKED_KEY = "own-mail";
|
|
64
90
|
export interface PlanOwnMailOptions {
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -6,12 +6,16 @@ import { brokerEntryAppliedScopeKey } from './broker-entries.js';
|
|
|
6
6
|
import { ZITADEL_FILES_PROJECT_SCOPE_KEY } from './core-files.js';
|
|
7
7
|
import { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, ownAuthApplied } from './login-host.js';
|
|
8
8
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, } from './mail-relay.js';
|
|
9
|
+
import { mailStoreIsLegacy, mailStoreSwitchDescription } from './mail-store.js';
|
|
9
10
|
import { ownHostnames } from './own-hostnames.js';
|
|
10
11
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
|
|
11
12
|
import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
|
|
12
13
|
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
|
|
13
14
|
import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
|
|
14
|
-
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
15
|
+
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
16
|
+
import { signinDbBackupMissing } from './signin-db-backup.js';
|
|
17
|
+
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, signinDbCertDue } from './signin-db-tls.js';
|
|
18
|
+
import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db.js';
|
|
15
19
|
/** `unmanaged` key of the one line `planOwnJobs` reports. */
|
|
16
20
|
export const OWN_JOBS_BLOCKED_KEY = 'own-jobs';
|
|
17
21
|
/**
|
|
@@ -129,6 +133,15 @@ export function planOwnAuth(options) {
|
|
|
129
133
|
if (!options.mailKeyHeld && !options.mailKeyMintable) {
|
|
130
134
|
return blocked(`the held key ${orgTarget(org).refs.brokerMailKey} is not held yet`);
|
|
131
135
|
}
|
|
136
|
+
// A company still on the Coolify database keeps running without the keys until it moves.
|
|
137
|
+
if (!recordedIds[PLATFORM_AUTH_DB_SCOPE_KEY]) {
|
|
138
|
+
if (options.signinDbKeysPuttable === false && !recordedIds[SIGNIN_DB_TLS_SCOPE_KEY]) {
|
|
139
|
+
return blocked('the host cannot store the sign-in database certificate (held keys have no put)');
|
|
140
|
+
}
|
|
141
|
+
if (options.signinDbBackupConfigured === false) {
|
|
142
|
+
return blocked('the host supplies no backup bucket for the sign-in database');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
132
145
|
const hostname = names.auth;
|
|
133
146
|
// The broker starts only on a valid `AUTH_APPS`, and the login client is minted only once it does:
|
|
134
147
|
// the first broker entry is what lets this step finish, as the platform's first registration does.
|
|
@@ -148,7 +161,11 @@ export function planOwnAuth(options) {
|
|
|
148
161
|
recordedIds[PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY] &&
|
|
149
162
|
recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY] &&
|
|
150
163
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
151
|
-
(Boolean(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]) || !brokerHoldsAnEntry)
|
|
164
|
+
(Boolean(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]) || !brokerHoldsAnEntry) &&
|
|
165
|
+
!signinDbCertDue(recordedIds, options.now ?? Date.now()) &&
|
|
166
|
+
!signinDbBackupMissing(recordedIds) &&
|
|
167
|
+
// ZITADEL was started on the TLS database and never seen healthy there: the step starts it again.
|
|
168
|
+
!recordedIds[SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY]) {
|
|
152
169
|
return { step: null, blocked: null };
|
|
153
170
|
}
|
|
154
171
|
return {
|
|
@@ -168,6 +185,47 @@ export function planOwnAuth(options) {
|
|
|
168
185
|
},
|
|
169
186
|
};
|
|
170
187
|
}
|
|
188
|
+
/** `unmanaged` key of the one line `planSigninDbMove` reports. */
|
|
189
|
+
export const OWN_SIGNIN_DB_MOVE_BLOCKED_KEY = 'own-signin-db-move';
|
|
190
|
+
/**
|
|
191
|
+
* One step for an own org whose sign-in is applied and whose ZITADEL still uses the Coolify database
|
|
192
|
+
* (#746): `signin-db-move.ts` copies it to foundry's TLS image. Never planned for a company on the TLS
|
|
193
|
+
* database, a new one, or one whose sign-in is not applied. The apply refuses it without a fresh
|
|
194
|
+
* backup of the old database (`backup-gate.ts`).
|
|
195
|
+
*/
|
|
196
|
+
export function planSigninDbMove(options) {
|
|
197
|
+
const { org, recordedIds } = options;
|
|
198
|
+
if (!ownsServices(org) ||
|
|
199
|
+
!ownAuthApplied(org, recordedIds) ||
|
|
200
|
+
!recordedIds[PLATFORM_AUTH_DB_SCOPE_KEY] ||
|
|
201
|
+
!recordedIds[PLATFORM_AUTH_ZITADEL_SCOPE_KEY] ||
|
|
202
|
+
recordedIds[SIGNIN_DB_TLS_SCOPE_KEY]) {
|
|
203
|
+
return { step: null, blocked: null };
|
|
204
|
+
}
|
|
205
|
+
if (options.signinDbKeysPuttable === false) {
|
|
206
|
+
return {
|
|
207
|
+
step: null,
|
|
208
|
+
blocked: `${org.slug}'s sign-in database move is blocked, not planned: the host cannot store the sign-in database certificate (held keys have no put)`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (options.signinDbBackupConfigured === false) {
|
|
212
|
+
return {
|
|
213
|
+
step: null,
|
|
214
|
+
blocked: `${org.slug}'s sign-in database move is blocked, not planned: the host supplies no backup bucket for the sign-in database`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
blocked: null,
|
|
219
|
+
step: {
|
|
220
|
+
action: { system: 'coolify', kind: 'own-auth-db-move', key: org.id, desired: {} },
|
|
221
|
+
step: {
|
|
222
|
+
description: `move ${org.slug}'s sign-in database to foundry's TLS image: ZITADEL is stopped while the data is copied, the old database is kept`,
|
|
223
|
+
destroys: false,
|
|
224
|
+
migrates: true,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
171
229
|
/** `unmanaged` key of the one line `planOwnMail` reports. */
|
|
172
230
|
export const OWN_MAIL_BLOCKED_KEY = 'own-mail';
|
|
173
231
|
/**
|
|
@@ -212,7 +270,9 @@ export function planOwnMail(options) {
|
|
|
212
270
|
const smtpPort = options.smtpPort ?? OWN_MAIL_SMTP_PORT_BASE;
|
|
213
271
|
if (!isOwnMailSmtpPort(smtpPort))
|
|
214
272
|
return blocked('its mail port is outside the range foundry uses');
|
|
215
|
-
|
|
273
|
+
const storeSwitch = mailStoreIsLegacy(recordedIds);
|
|
274
|
+
if (!storeSwitch &&
|
|
275
|
+
recordedIds[`coolify:own-mail:${org.id}`] &&
|
|
216
276
|
recordedIds[OWN_MAIL_SMTP_PORT_SCOPE_KEY] === String(smtpPort) &&
|
|
217
277
|
recordedIds[PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY] === hostname &&
|
|
218
278
|
recordedIds[PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY] === gitRef &&
|
|
@@ -227,11 +287,19 @@ export function planOwnMail(options) {
|
|
|
227
287
|
system: 'coolify',
|
|
228
288
|
kind: 'own-mail',
|
|
229
289
|
key: org.id,
|
|
230
|
-
desired: {
|
|
290
|
+
desired: {
|
|
291
|
+
hostname,
|
|
292
|
+
gitRef,
|
|
293
|
+
mailHost: platformMailMxHost(platformApex),
|
|
294
|
+
smtpPort,
|
|
295
|
+
...(storeSwitch ? { switchMailStore: true } : {}),
|
|
296
|
+
},
|
|
231
297
|
},
|
|
232
298
|
step: {
|
|
233
|
-
description:
|
|
234
|
-
|
|
299
|
+
description: storeSwitch
|
|
300
|
+
? mailStoreSwitchDescription(org.slug)
|
|
301
|
+
: `provision ${org.slug}'s own mail server`,
|
|
302
|
+
destroys: storeSwitch,
|
|
235
303
|
migrates: false,
|
|
236
304
|
},
|
|
237
305
|
},
|
|
@@ -20,12 +20,17 @@ export interface ServiceTarget {
|
|
|
20
20
|
readonly kind: 'platform' | 'org';
|
|
21
21
|
readonly names: {
|
|
22
22
|
authDb: string;
|
|
23
|
+
/** The sign-in database as foundry's own TLS image (#746). Only an org uses it; the platform's value is never read. */
|
|
24
|
+
signinDb: string;
|
|
25
|
+
/** The one-shot job that copies the old database into it (#746). */
|
|
26
|
+
signinDbMove: string;
|
|
23
27
|
authZitadel: string;
|
|
24
28
|
authLogin: string;
|
|
25
29
|
authBroker: string;
|
|
26
30
|
authTunnel: string;
|
|
27
31
|
authCloudflared: string;
|
|
28
32
|
mailApp: string;
|
|
33
|
+
mailDb: string;
|
|
29
34
|
mailTunnel: string;
|
|
30
35
|
mailCloudflared: string;
|
|
31
36
|
filesDb: string;
|
|
@@ -33,6 +38,8 @@ export interface ServiceTarget {
|
|
|
33
38
|
filesRunner: string;
|
|
34
39
|
/** An own org's R2 bucket. The platform's is host-supplied (`PlatformFilesDeps.r2`), so its value here is never read. */
|
|
35
40
|
filesBucket: string;
|
|
41
|
+
/** An own org's sign-in database backup bucket (#746): its own bucket, never the files bucket. */
|
|
42
|
+
signinBackupBucket: string;
|
|
36
43
|
jobsApp: string;
|
|
37
44
|
jobsDb: string;
|
|
38
45
|
sfuApp: string;
|
|
@@ -40,6 +47,8 @@ export interface ServiceTarget {
|
|
|
40
47
|
sfuCloudflared: string;
|
|
41
48
|
};
|
|
42
49
|
readonly internal: {
|
|
50
|
+
/** The hostname ZITADEL connects to, and the one name the database's certificate carries. */
|
|
51
|
+
signinDb: string;
|
|
43
52
|
authZitadel: string;
|
|
44
53
|
authLogin: string;
|
|
45
54
|
authBroker: string;
|
|
@@ -61,6 +70,8 @@ export interface ServiceTarget {
|
|
|
61
70
|
keysCredential: string;
|
|
62
71
|
/** The public half the host verifies with (`KEYS_VERIFY_KEYS`). */
|
|
63
72
|
keysVerifyKeys: string;
|
|
73
|
+
/** The sign-in database's server certificate and key, held as JSON `{ cert, key }` (#746). */
|
|
74
|
+
signinDbServer: string;
|
|
64
75
|
};
|
|
65
76
|
}
|
|
66
77
|
/** The platform's shared services, with the literals the platform steps use today. */
|
|
@@ -11,18 +11,22 @@ export function platformTarget() {
|
|
|
11
11
|
kind: 'platform',
|
|
12
12
|
names: {
|
|
13
13
|
authDb: 'platform-auth-db',
|
|
14
|
+
signinDb: 'platform-signin-db',
|
|
15
|
+
signinDbMove: 'platform-signin-db-move',
|
|
14
16
|
authZitadel: 'platform-auth-zitadel',
|
|
15
17
|
authLogin: 'platform-auth-login',
|
|
16
18
|
authBroker: 'platform-auth-broker',
|
|
17
19
|
authTunnel: 'platform-auth-tunnel',
|
|
18
20
|
authCloudflared: 'platform-auth-cloudflared',
|
|
19
21
|
mailApp: 'platform-mail-stalwart',
|
|
22
|
+
mailDb: 'platform-mail-postgres',
|
|
20
23
|
mailTunnel: 'platform-mail-tunnel',
|
|
21
24
|
mailCloudflared: 'platform-mail-cloudflared',
|
|
22
25
|
filesDb: 'platform-files-db',
|
|
23
26
|
filesMigrate: 'files-migrate',
|
|
24
27
|
filesRunner: 'files',
|
|
25
28
|
filesBucket: 'platform-files',
|
|
29
|
+
signinBackupBucket: 'platform-signin-backups',
|
|
26
30
|
jobsApp: 'jobs',
|
|
27
31
|
jobsDb: 'jobs-scheduler-db',
|
|
28
32
|
sfuApp: 'platform-sfu',
|
|
@@ -30,6 +34,7 @@ export function platformTarget() {
|
|
|
30
34
|
sfuCloudflared: 'platform-sfu-cloudflared',
|
|
31
35
|
},
|
|
32
36
|
internal: {
|
|
37
|
+
signinDb: 'platform-signin-db',
|
|
33
38
|
authZitadel: 'platform-auth-zitadel',
|
|
34
39
|
authLogin: 'platform-auth-login',
|
|
35
40
|
authBroker: 'platform-auth-broker',
|
|
@@ -49,6 +54,7 @@ export function platformTarget() {
|
|
|
49
54
|
livekitKeys: 'livekit:platform:keys',
|
|
50
55
|
keysCredential: 'keys:platform:credential',
|
|
51
56
|
keysVerifyKeys: 'keys:platform:verifyKeys',
|
|
57
|
+
signinDbServer: 'signin-db:platform:server',
|
|
52
58
|
},
|
|
53
59
|
};
|
|
54
60
|
}
|
|
@@ -60,18 +66,22 @@ export function orgTarget(org) {
|
|
|
60
66
|
kind: 'org',
|
|
61
67
|
names: {
|
|
62
68
|
authDb: `${s}-auth-db`,
|
|
69
|
+
signinDb: `${s}-signin-db`,
|
|
70
|
+
signinDbMove: `${s}-signin-db-move`,
|
|
63
71
|
authZitadel: `${s}-auth-zitadel`,
|
|
64
72
|
authLogin: `${s}-auth-login`,
|
|
65
73
|
authBroker: `${s}-auth-broker`,
|
|
66
74
|
authTunnel: `${s}-auth-tunnel`,
|
|
67
75
|
authCloudflared: `${s}-auth-cloudflared`,
|
|
68
76
|
mailApp: `${s}-mail-stalwart`,
|
|
77
|
+
mailDb: `${s}-mail-postgres`,
|
|
69
78
|
mailTunnel: `${s}-mail-tunnel`,
|
|
70
79
|
mailCloudflared: `${s}-mail-cloudflared`,
|
|
71
80
|
filesDb: `${s}-files-db`,
|
|
72
81
|
filesMigrate: `${s}-files-migrate`,
|
|
73
82
|
filesRunner: `${s}-files`,
|
|
74
83
|
filesBucket: `${s}-files`,
|
|
84
|
+
signinBackupBucket: `${s}-signin-backups`,
|
|
75
85
|
jobsApp: `${s}-jobs`,
|
|
76
86
|
jobsDb: `${s}-jobs-db`,
|
|
77
87
|
sfuApp: `${s}-sfu`,
|
|
@@ -79,6 +89,7 @@ export function orgTarget(org) {
|
|
|
79
89
|
sfuCloudflared: `${s}-sfu-cloudflared`,
|
|
80
90
|
},
|
|
81
91
|
internal: {
|
|
92
|
+
signinDb: `${s}-signin-db`,
|
|
82
93
|
authZitadel: `${s}-auth-zitadel`,
|
|
83
94
|
authLogin: `${s}-auth-login`,
|
|
84
95
|
authBroker: `${s}-auth-broker`,
|
|
@@ -98,6 +109,7 @@ export function orgTarget(org) {
|
|
|
98
109
|
livekitKeys: `livekit:org:${s}:keys`,
|
|
99
110
|
keysCredential: `keys:org:${s}:credential`,
|
|
100
111
|
keysVerifyKeys: `keys:org:${s}:verifyKeys`,
|
|
112
|
+
signinDbServer: `signin-db:org:${s}:server`,
|
|
101
113
|
},
|
|
102
114
|
};
|
|
103
115
|
}
|
|
@@ -199,6 +199,10 @@ export interface StepExecutor {
|
|
|
199
199
|
}>;
|
|
200
200
|
/** Whether the executor can mint an own org's Lettermint key: a Lettermint port and a reader for the held ref. */
|
|
201
201
|
ownMailKeyMintable?(): boolean;
|
|
202
|
+
/** Whether the executor's held keys can store the sign-in database's CA and certificate (#746). Absent: true. */
|
|
203
|
+
ownSigninDbKeysPuttable?(): boolean;
|
|
204
|
+
/** Whether the host supplies the backup bucket the sign-in database dumps into (#746). Absent: true. */
|
|
205
|
+
ownSigninDbBackupConfigured?(): boolean;
|
|
202
206
|
/**
|
|
203
207
|
* Whether the host's scoped ports for this own org include a files-organisation
|
|
204
208
|
* port. The planner plans no `files:organisation` step for an own org while
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -7,7 +7,7 @@ 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, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
10
|
+
import { NetworkMoveBackupMissingError, SigninDbMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupForSigninMove, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
11
11
|
import { readLiveBuilds } from './build-record.js';
|
|
12
12
|
import { unconfirmedUpgrades } from './confirm-upgrade.js';
|
|
13
13
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
@@ -22,7 +22,7 @@ import { findNameClashes, nameClashMessage } from './org-names.js';
|
|
|
22
22
|
import { ownHostnames } from './own-hostnames.js';
|
|
23
23
|
import { OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, orgTarget } from './own-services.js';
|
|
24
24
|
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';
|
|
25
|
-
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, dnsBlockedReason, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, ownerFilesFrom, ownerSignInFrom, platformOwnerUserIdFrom, serviceGitRefsOf, } from './platform-org.js';
|
|
25
|
+
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, dnsBlockedReason, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, ownerChatFrom, ownerFilesFrom, ownerSignInFrom, platformOwnerUserIdFrom, serviceGitRefsOf, } from './platform-org.js';
|
|
26
26
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY, } from './platform-providers.js';
|
|
27
27
|
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, } from './proxy-labels.js';
|
|
28
28
|
import { liveWorkers, workerTraceScopeKeys } from './worker.js';
|
|
@@ -443,6 +443,25 @@ export class FoundryPlanApply {
|
|
|
443
443
|
throw e;
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
+
// The move of a sign-in database reads the old one, and the old one stays; a fresh dump is the way back.
|
|
447
|
+
if (detail.plannedSteps.some((s) => s.action.system === 'coolify' && s.action.kind === 'own-auth-db-move')) {
|
|
448
|
+
try {
|
|
449
|
+
if (!this.backupGate)
|
|
450
|
+
throw new SigninDbMoveBackupMissingError('no-gate');
|
|
451
|
+
await requireFreshBackupForSigninMove({
|
|
452
|
+
coolify: this.backupGate.coolify,
|
|
453
|
+
orgId,
|
|
454
|
+
recordedIds: (await this.store.getAppliedState(orgId))?.serviceIds ?? {},
|
|
455
|
+
now: () => this.now().getTime(),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
if (e instanceof SigninDbMoveBackupMissingError) {
|
|
460
|
+
await this.refuse(plan, 'sign-in database move without a fresh backup');
|
|
461
|
+
}
|
|
462
|
+
throw e;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
446
465
|
if (planNeedsConfirmation(plan.steps) &&
|
|
447
466
|
confirmedOrgName.trim() !== org.displayName.trim()) {
|
|
448
467
|
await this.refuse(plan, "destructive or migrating step without the org's name typed correctly");
|
|
@@ -1044,6 +1063,8 @@ export class FoundryPlanApply {
|
|
|
1044
1063
|
oikosZoneReady: platformReadiness.oikosIngressApplied === true,
|
|
1045
1064
|
ownMailKeyHeld: await this.ownMailKeyHeld(org),
|
|
1046
1065
|
ownMailKeyMintable: this.executor.ownMailKeyMintable?.() ?? false,
|
|
1066
|
+
ownSigninDbKeysPuttable: this.executor.ownSigninDbKeysPuttable?.() ?? true,
|
|
1067
|
+
ownSigninDbBackupConfigured: this.executor.ownSigninDbBackupConfigured?.() ?? true,
|
|
1047
1068
|
...(this.backupGate
|
|
1048
1069
|
? {
|
|
1049
1070
|
backupStorageUuid: (await this.platformDeps?.store.getServiceIds())?.[PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY] || null,
|
|
@@ -1330,9 +1351,13 @@ export class FoundryPlanApply {
|
|
|
1330
1351
|
async platformOwnerInput(platform) {
|
|
1331
1352
|
const owner = await this.store.get(platform.ownerOrgId);
|
|
1332
1353
|
if (!owner)
|
|
1333
|
-
return { signIn: null, files: null };
|
|
1354
|
+
return { signIn: null, files: null, chat: null };
|
|
1334
1355
|
const recorded = (await this.store.getAppliedState(owner.id))?.serviceIds ?? {};
|
|
1335
|
-
return {
|
|
1356
|
+
return {
|
|
1357
|
+
signIn: ownerSignInFrom(owner, recorded),
|
|
1358
|
+
files: ownerFilesFrom(owner, recorded),
|
|
1359
|
+
chat: ownerChatFrom(owner, recorded),
|
|
1360
|
+
};
|
|
1336
1361
|
}
|
|
1337
1362
|
/**
|
|
1338
1363
|
* What the shared mail relay needs from the orgs. A route exists for an org
|
|
@@ -1686,6 +1711,7 @@ const STEP_FIELDS = {
|
|
|
1686
1711
|
'coolify:mail': 'mail',
|
|
1687
1712
|
'coolify:chat': 'chat.usesDomain',
|
|
1688
1713
|
'coolify:own-auth': 'services.own',
|
|
1714
|
+
'coolify:own-auth-db-move': 'services.own',
|
|
1689
1715
|
'coolify:own-files': 'services.own',
|
|
1690
1716
|
'coolify:own-files-r2': 'services.own',
|
|
1691
1717
|
'coolify:own-jobs': 'services.own',
|
|
@@ -1030,6 +1030,25 @@ export declare const PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY = "uptime_applied
|
|
|
1030
1030
|
export declare const PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY = "uptime_applied_vars";
|
|
1031
1031
|
/** `'1'` once the step removed the Worker because the platform no longer enables it; cleared when it deploys again. */
|
|
1032
1032
|
export declare const PLATFORM_UPTIME_REMOVED_SCOPE_KEY = "uptime_removed";
|
|
1033
|
+
/**
|
|
1034
|
+
* The chat bot's room (`!id:server`), recorded as soon as chat-service names it.
|
|
1035
|
+
* Recorded before the bot's token is held, so a crash between the two leaves the room known.
|
|
1036
|
+
*/
|
|
1037
|
+
export declare const PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY = "platform_uptime_chat_room_id";
|
|
1038
|
+
/**
|
|
1039
|
+
* A random id, recorded and persisted before the chat route is called (it mints the bot's token) and
|
|
1040
|
+
* cleared once that token is held. Set while the token is not held, it means a token was minted that may
|
|
1041
|
+
* be lost, so the next call asks the route to rotate it.
|
|
1042
|
+
*/
|
|
1043
|
+
export declare const PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY = "platform_uptime_chat_token_undelivered";
|
|
1044
|
+
/**
|
|
1045
|
+
* The Worker's plain text variables: the chat homeserver and room foundry made, under whatever the
|
|
1046
|
+
* host configured (a host value that is set wins). Unset values are left out.
|
|
1047
|
+
*/
|
|
1048
|
+
export declare function uptimeEffectiveVars(hostVars: Readonly<Record<string, string | undefined>> | undefined, chat: {
|
|
1049
|
+
homeserverUrl: string;
|
|
1050
|
+
roomId: string;
|
|
1051
|
+
} | null): Record<string, string | undefined>;
|
|
1033
1052
|
/** The digest of the Worker's plain text variables that are set; empty when none is. */
|
|
1034
1053
|
export declare function uptimeVarsDigest(vars?: Readonly<Record<string, string | undefined>>): string;
|
|
1035
1054
|
/** The Worker secret names the step sets: the three it mints, and each held one. */
|
|
@@ -1164,6 +1183,16 @@ export interface PlanPlatformOptions {
|
|
|
1164
1183
|
access: boolean;
|
|
1165
1184
|
vars: string;
|
|
1166
1185
|
};
|
|
1186
|
+
/**
|
|
1187
|
+
* The uptime Worker's chat bot, when the host wired one. `pending`: the bot's token is not held and
|
|
1188
|
+
* Company Included's chat is applied, so the step makes it. `blocked`: why it cannot yet (a fixed
|
|
1189
|
+
* line, reported without stopping the rest of the Worker). Absent: no bot path, so the plan keeps
|
|
1190
|
+
* asking for a hand-held credential.
|
|
1191
|
+
*/
|
|
1192
|
+
uptimeChat?: {
|
|
1193
|
+
pending: boolean;
|
|
1194
|
+
blocked?: string;
|
|
1195
|
+
};
|
|
1167
1196
|
/** Why Archon's step must wait: the deployment it triggered is still running. Blocks even an applied step. */
|
|
1168
1197
|
archonDeployRunning?: string;
|
|
1169
1198
|
/**
|
|
@@ -1279,6 +1308,7 @@ export declare const PLATFORM_BACKUP_DATABASE_SCOPE_KEYS: {
|
|
|
1279
1308
|
readonly archon: "coolify_platform_archon_database";
|
|
1280
1309
|
readonly ai: "coolify_platform_ai_database";
|
|
1281
1310
|
readonly integrations: "coolify_integrations_database";
|
|
1311
|
+
readonly mail: "coolify_mail_database";
|
|
1282
1312
|
};
|
|
1283
1313
|
export type PlatformBackupServiceKey = keyof typeof PLATFORM_BACKUP_DATABASE_SCOPE_KEYS;
|
|
1284
1314
|
export declare const PLATFORM_BACKUP_SERVICE_KEYS: readonly PlatformBackupServiceKey[];
|
|
@@ -7,6 +7,7 @@ import { versionChangeNeedsConfirmation } from './confirm-upgrade.js';
|
|
|
7
7
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
8
8
|
import { legacyAppHostname } from './hostnames.js';
|
|
9
9
|
import { PLATFORM_MAIL_RELAY_KEY, planMailRelay } from './mail-relay.js';
|
|
10
|
+
import { MAIL_DATABASE_SCOPE_KEY, mailStoreIsLegacy, mailStoreSwitchChange, mailStoreSwitchDescription, } from './mail-store.js';
|
|
10
11
|
import { planOikosIngress } from './oikos-ingress.js';
|
|
11
12
|
import { callsSfuAppliedFingerprint } from './platform-sfu.js';
|
|
12
13
|
import { signInDomainBlockedText } from './sign-in-org.js';
|
|
@@ -1155,6 +1156,28 @@ export const PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY = 'uptime_applied_secrets
|
|
|
1155
1156
|
export const PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY = 'uptime_applied_vars';
|
|
1156
1157
|
/** `'1'` once the step removed the Worker because the platform no longer enables it; cleared when it deploys again. */
|
|
1157
1158
|
export const PLATFORM_UPTIME_REMOVED_SCOPE_KEY = 'uptime_removed';
|
|
1159
|
+
/**
|
|
1160
|
+
* The chat bot's room (`!id:server`), recorded as soon as chat-service names it.
|
|
1161
|
+
* Recorded before the bot's token is held, so a crash between the two leaves the room known.
|
|
1162
|
+
*/
|
|
1163
|
+
export const PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY = 'platform_uptime_chat_room_id';
|
|
1164
|
+
/**
|
|
1165
|
+
* A random id, recorded and persisted before the chat route is called (it mints the bot's token) and
|
|
1166
|
+
* cleared once that token is held. Set while the token is not held, it means a token was minted that may
|
|
1167
|
+
* be lost, so the next call asks the route to rotate it.
|
|
1168
|
+
*/
|
|
1169
|
+
export const PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY = 'platform_uptime_chat_token_undelivered';
|
|
1170
|
+
/**
|
|
1171
|
+
* The Worker's plain text variables: the chat homeserver and room foundry made, under whatever the
|
|
1172
|
+
* host configured (a host value that is set wins). Unset values are left out.
|
|
1173
|
+
*/
|
|
1174
|
+
export function uptimeEffectiveVars(hostVars, chat) {
|
|
1175
|
+
const host = Object.fromEntries(Object.entries(hostVars ?? {}).filter(([, v]) => Boolean(v)));
|
|
1176
|
+
return {
|
|
1177
|
+
...(chat ? { CHAT_HOMESERVER_URL: chat.homeserverUrl, CHAT_ROOM_ID: chat.roomId } : {}),
|
|
1178
|
+
...host,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1158
1181
|
/** The digest of the Worker's plain text variables that are set; empty when none is. */
|
|
1159
1182
|
export function uptimeVarsDigest(vars = {}) {
|
|
1160
1183
|
const set = Object.entries(vars)
|
|
@@ -1332,7 +1355,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1332
1355
|
plannedSteps.push(heartbeats.step);
|
|
1333
1356
|
// foundry#542: after Archon is applied (never merely planned): the Worker
|
|
1334
1357
|
// probes Archon, and Archon's env takes the token this step mints.
|
|
1335
|
-
const uptime = planUptime(platform, recordedIds, blocked.uptime, blocked.uptimeSecretsHeld ?? [], Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_ARCHON_KEY)]), blocked.uptimeHeld);
|
|
1358
|
+
const uptime = planUptime(platform, recordedIds, blocked.uptime, blocked.uptimeSecretsHeld ?? [], Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_ARCHON_KEY)]), blocked.uptimeHeld, blocked.uptimeChat);
|
|
1336
1359
|
if (uptime.step)
|
|
1337
1360
|
plannedSteps.push(uptime.step);
|
|
1338
1361
|
if (archon.step && blocked.archonRunsInsideApp)
|
|
@@ -1353,6 +1376,9 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1353
1376
|
// read for, is premature, not informational.
|
|
1354
1377
|
const versionChanges = [];
|
|
1355
1378
|
for (const change of [
|
|
1379
|
+
mail.blocked || !mail.step?.action.desired.switchMailStore
|
|
1380
|
+
? null
|
|
1381
|
+
: mailStoreSwitchChange('the platform'),
|
|
1356
1382
|
mail.blocked
|
|
1357
1383
|
? null
|
|
1358
1384
|
: versionChangeLine('mail', recordedIds[PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY], platform.mail.gitRef, 'image, redeploy'),
|
|
@@ -1593,11 +1619,13 @@ export const PLATFORM_BACKUP_DATABASE_SCOPE_KEYS = {
|
|
|
1593
1619
|
archon: 'coolify_platform_archon_database',
|
|
1594
1620
|
ai: 'coolify_platform_ai_database',
|
|
1595
1621
|
integrations: 'coolify_integrations_database',
|
|
1622
|
+
mail: MAIL_DATABASE_SCOPE_KEY,
|
|
1596
1623
|
};
|
|
1597
1624
|
export const PLATFORM_BACKUP_SERVICE_KEYS = [
|
|
1598
1625
|
'archon',
|
|
1599
1626
|
'ai',
|
|
1600
1627
|
'integrations',
|
|
1628
|
+
'mail',
|
|
1601
1629
|
];
|
|
1602
1630
|
/**
|
|
1603
1631
|
* #257: a nightly backup for each platform database foundry itself creates,
|
|
@@ -2047,7 +2075,9 @@ function planMail(platform, recordedIds, executorBlocked, authReady) {
|
|
|
2047
2075
|
if (!platform.mail.enabled)
|
|
2048
2076
|
return { step: null, blocked: null };
|
|
2049
2077
|
const key = resourceKey('coolify', 'app', PLATFORM_MAIL_KEY);
|
|
2050
|
-
|
|
2078
|
+
const storeSwitch = mailStoreIsLegacy(recordedIds);
|
|
2079
|
+
if (!storeSwitch &&
|
|
2080
|
+
recordedIds[key] &&
|
|
2051
2081
|
recordedIds[PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY] &&
|
|
2052
2082
|
recordedIds[PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY] === platform.mail.hostname &&
|
|
2053
2083
|
recordedIds[PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY] === platform.mail.gitRef &&
|
|
@@ -2083,6 +2113,28 @@ function planMail(platform, recordedIds, executorBlocked, authReady) {
|
|
|
2083
2113
|
blocked: "the platform's mail server is blocked, not planned: its OIDC directory uses the platform's own sign-in, which is neither applied nor planned",
|
|
2084
2114
|
};
|
|
2085
2115
|
}
|
|
2116
|
+
if (storeSwitch) {
|
|
2117
|
+
return {
|
|
2118
|
+
step: {
|
|
2119
|
+
action: {
|
|
2120
|
+
system: 'coolify',
|
|
2121
|
+
kind: 'app',
|
|
2122
|
+
key: PLATFORM_MAIL_KEY,
|
|
2123
|
+
desired: {
|
|
2124
|
+
hostname: platform.mail.hostname,
|
|
2125
|
+
gitRef: platform.mail.gitRef,
|
|
2126
|
+
switchMailStore: true,
|
|
2127
|
+
},
|
|
2128
|
+
},
|
|
2129
|
+
step: {
|
|
2130
|
+
description: mailStoreSwitchDescription('the platform'),
|
|
2131
|
+
destroys: true,
|
|
2132
|
+
migrates: false,
|
|
2133
|
+
},
|
|
2134
|
+
},
|
|
2135
|
+
blocked: null,
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2086
2138
|
return {
|
|
2087
2139
|
step: {
|
|
2088
2140
|
action: {
|
|
@@ -2684,7 +2736,7 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2684
2736
|
token: true,
|
|
2685
2737
|
access: true,
|
|
2686
2738
|
vars: '',
|
|
2687
|
-
}) {
|
|
2739
|
+
}, chat) {
|
|
2688
2740
|
const { uptime } = platform;
|
|
2689
2741
|
const deployed = Boolean(recordedIds[resourceKey('cloudflare', 'worker', PLATFORM_UPTIME_KEY)]) &&
|
|
2690
2742
|
!recordedIds[PLATFORM_UPTIME_REMOVED_SCOPE_KEY];
|
|
@@ -2733,7 +2785,7 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2733
2785
|
if (executorBlocked)
|
|
2734
2786
|
return { step: null, blocked: [executorBlocked], notes: [] };
|
|
2735
2787
|
const notes = [];
|
|
2736
|
-
if (!heldOptional.includes('CHAT_ACCESS_TOKEN')) {
|
|
2788
|
+
if (!chat && !heldOptional.includes('CHAT_ACCESS_TOKEN')) {
|
|
2737
2789
|
notes.push({
|
|
2738
2790
|
key: 'uptime:CHAT_ACCESS_TOKEN',
|
|
2739
2791
|
service: 'uptime',
|
|
@@ -2759,9 +2811,12 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2759
2811
|
recordedIds[PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY] === uptime.sha256 &&
|
|
2760
2812
|
recordedIds[PLATFORM_UPTIME_APPLIED_HOSTNAME_SCOPE_KEY] === hostname &&
|
|
2761
2813
|
(recordedIds[PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY] ?? '') === uptimeHeld.vars &&
|
|
2762
|
-
recordedIds[PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY] === uptimeSecretNames(heldOptional)
|
|
2814
|
+
recordedIds[PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY] === uptimeSecretNames(heldOptional) &&
|
|
2815
|
+
!chat?.pending;
|
|
2816
|
+
// Reported beside the step, not instead of it: the Worker deploys without chat alerts meanwhile.
|
|
2817
|
+
const blockedLines = chat?.blocked ? [chat.blocked] : [];
|
|
2763
2818
|
if (applied)
|
|
2764
|
-
return { step: null, blocked:
|
|
2819
|
+
return { step: null, blocked: blockedLines, notes };
|
|
2765
2820
|
return {
|
|
2766
2821
|
step: {
|
|
2767
2822
|
action: action({ hostname, gitRef: uptime.tag, sha256: uptime.sha256 }),
|
|
@@ -2771,7 +2826,7 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
2771
2826
|
migrates: false,
|
|
2772
2827
|
},
|
|
2773
2828
|
},
|
|
2774
|
-
blocked:
|
|
2829
|
+
blocked: blockedLines,
|
|
2775
2830
|
notes,
|
|
2776
2831
|
};
|
|
2777
2832
|
}
|
|
@@ -304,10 +304,23 @@ export interface OwnerFiles {
|
|
|
304
304
|
organisationId: string;
|
|
305
305
|
managementKeyRef: string;
|
|
306
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* The owner company's chat once `coolify:chat` has applied: where the uptime
|
|
309
|
+
* Worker's bot is made and which people join its room.
|
|
310
|
+
*/
|
|
311
|
+
export interface OwnerChat {
|
|
312
|
+
/** The chat-service app (Coolify uuid), whose env holds `CHAT_INTERNAL_TOKEN`. */
|
|
313
|
+
appId: string;
|
|
314
|
+
/** The company's Agora host: the public origin of the bot route and the Matrix homeserver. */
|
|
315
|
+
agoraHost: string;
|
|
316
|
+
/** ZITADEL subjects of the owner company's owners whose invite is recorded. */
|
|
317
|
+
subjects: readonly string[];
|
|
318
|
+
}
|
|
307
319
|
/** What the platform plan reads about the owner company. */
|
|
308
320
|
export interface PlatformOwnerInput {
|
|
309
321
|
signIn?: OwnerSignIn | null;
|
|
310
322
|
files?: OwnerFiles | null;
|
|
323
|
+
chat?: OwnerChat | null;
|
|
311
324
|
}
|
|
312
325
|
/**
|
|
313
326
|
* The owner's sign-in from its declared org and the ids its own steps recorded.
|
|
@@ -326,3 +339,5 @@ export declare function ownerSignInFrom(owner: DeclaredOrg | null, recorded: Rea
|
|
|
326
339
|
export declare function platformOwnerUserIdFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): string | null;
|
|
327
340
|
/** The owner's files host once its own files step has applied; `null` before. */
|
|
328
341
|
export declare function ownerFilesFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): OwnerFiles | null;
|
|
342
|
+
/** The owner's chat once its `coolify:chat` step has applied; `null` before, and when chat is off. */
|
|
343
|
+
export declare function ownerChatFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): OwnerChat | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ownsServices } from '../org.js';
|
|
2
|
-
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
2
|
+
import { ZITADEL_ORG_SCOPE_KEY, agoraHostnameFor } from './agora.js';
|
|
3
3
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
4
4
|
import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
5
5
|
import { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, ownAuthApplied } from './login-host.js';
|
|
@@ -352,3 +352,19 @@ export function ownerFilesFrom(owner, recorded) {
|
|
|
352
352
|
managementKeyRef: orgTarget(owner).refs.filesManagement,
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
|
+
/** The recorded id of the chat-service app (`coolify_chat_service`, set by the `coolify:chat` step). */
|
|
356
|
+
const CHAT_SERVICE_APP_SCOPE_KEY = 'coolify_chat_service';
|
|
357
|
+
/** The owner's chat once its `coolify:chat` step has applied; `null` before, and when chat is off. */
|
|
358
|
+
export function ownerChatFrom(owner, recorded) {
|
|
359
|
+
const appId = recorded[CHAT_SERVICE_APP_SCOPE_KEY];
|
|
360
|
+
if (!owner || !owner.chat.enabled || !appId || !recorded[`coolify:chat:${owner.id}`])
|
|
361
|
+
return null;
|
|
362
|
+
return {
|
|
363
|
+
appId,
|
|
364
|
+
agoraHost: agoraHostnameFor(owner),
|
|
365
|
+
subjects: owner.owners.flatMap((o) => {
|
|
366
|
+
const id = recorded[`zitadel:owner:${owner.id}:${o.email.toLowerCase()}`];
|
|
367
|
+
return id ? [id] : [];
|
|
368
|
+
}),
|
|
369
|
+
};
|
|
370
|
+
}
|