@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.
Files changed (39) hide show
  1. package/dist/bootstrap/versions.d.ts +3 -2
  2. package/dist/index.d.ts +1 -1
  3. package/dist/plan/backup-gate.d.ts +19 -3
  4. package/dist/plan/backup-gate.js +49 -4
  5. package/dist/plan/company-backups.d.ts +7 -2
  6. package/dist/plan/company-backups.js +25 -6
  7. package/dist/plan/diff.d.ts +4 -0
  8. package/dist/plan/diff.js +19 -1
  9. package/dist/plan/executor.d.ts +13 -0
  10. package/dist/plan/executor.js +56 -13
  11. package/dist/plan/index.d.ts +2 -1
  12. package/dist/plan/index.js +1 -0
  13. package/dist/plan/mail-store.d.ts +34 -0
  14. package/dist/plan/mail-store.js +49 -0
  15. package/dist/plan/own-diff.d.ts +26 -0
  16. package/dist/plan/own-diff.js +74 -6
  17. package/dist/plan/own-services.d.ts +11 -0
  18. package/dist/plan/own-services.js +12 -0
  19. package/dist/plan/plan-apply.d.ts +4 -0
  20. package/dist/plan/plan-apply.js +30 -4
  21. package/dist/plan/platform-diff.d.ts +30 -0
  22. package/dist/plan/platform-diff.js +62 -7
  23. package/dist/plan/platform-org.d.ts +15 -0
  24. package/dist/plan/platform-org.js +17 -1
  25. package/dist/plan/platform-providers.d.ts +69 -2
  26. package/dist/plan/platform-providers.js +255 -41
  27. package/dist/plan/signin-db-backup.d.ts +105 -0
  28. package/dist/plan/signin-db-backup.js +193 -0
  29. package/dist/plan/signin-db-move.d.ts +68 -0
  30. package/dist/plan/signin-db-move.js +338 -0
  31. package/dist/plan/signin-db-tls.d.ts +69 -0
  32. package/dist/plan/signin-db-tls.js +255 -0
  33. package/dist/plan/signin-db.d.ts +114 -0
  34. package/dist/plan/signin-db.js +255 -0
  35. package/dist/plan/uptime-worker.d.ts +20 -1
  36. package/dist/plan/uptime-worker.js +121 -3
  37. package/package.json +1 -1
  38. package/versions.digests.json +2 -1
  39. package/versions.json +2 -1
@@ -2,8 +2,7 @@
2
2
  * The estate's pinned third-party image/release refs for the platform
3
3
  * services foundry provisions (#247). One JSON file, so Renovate can read
4
4
  * and later propose bumps against it, instead of four separate string
5
- * constants it cannot see. `postgres` has no entry here: no planner step
6
- * pins a postgres image tag today, so there is no ref to record yet.
5
+ * constants it cannot see.
7
6
  */
8
7
  export interface PinnedVersions {
9
8
  zitadel: string;
@@ -17,6 +16,8 @@ export interface PinnedVersions {
17
16
  alpine: string;
18
17
  /** A static busybox, copied into the MAS image so its health check has an HTTP client. */
19
18
  busybox: string;
19
+ /** The sign-in database's image (#746). Its major version is the data directory's format: a bump past 16 needs a dump and restore, not an image change. */
20
+ postgres: string;
20
21
  }
21
22
  export declare function loadVersions(): PinnedVersions;
22
23
  export declare const VERSIONS: PinnedVersions;
package/dist/index.d.ts CHANGED
@@ -54,7 +54,7 @@ export { checkMailIdentityAudience } from './plan/index.js';
54
54
  export type { MailAllowlistApp } from './plan/index.js';
55
55
  export { checkMailAppsAllowlist } from './plan/index.js';
56
56
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
57
- export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
57
+ export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeChatDeps, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
58
58
  export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
59
59
  export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
60
60
  export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
@@ -28,6 +28,15 @@ export declare class NetworkMoveBackupMissingError extends Error {
28
28
  readonly reason: 'no-gate' | 'no-backup' | 'by-hand';
29
29
  constructor(reason: 'no-gate' | 'no-backup' | 'by-hand');
30
30
  }
31
+ /**
32
+ * The move of a company's sign-in database to the TLS image (#746) is refused before any step runs when
33
+ * the old database has no finished backup from the last {@link BACKUP_MAX_AGE_HOURS} hours, or the host
34
+ * has no backup gate. The message names no database, uuid or URL. There is no override.
35
+ */
36
+ export declare class SigninDbMoveBackupMissingError extends Error {
37
+ readonly reason: 'no-gate' | 'no-backup';
38
+ constructor(reason: 'no-gate' | 'no-backup');
39
+ }
31
40
  export interface BackupGatePorts {
32
41
  /** The company's recorded ids (`AppliedState.serviceIds`): database uuids and `backup_<db>` schedule uuids. */
33
42
  recordedIds: Readonly<Record<string, string>>;
@@ -52,8 +61,8 @@ export interface BackupGatePorts {
52
61
  * `unconfirmedUpgrades` and before any step. A service whose database the
53
62
  * company does not have passes (there is nothing to migrate); a database that
54
63
  * exists with no recorded schedule, a disabled or non-S3 schedule, a failed or
55
- * old dump, or an unreadable list all refuse. `mail` has no Postgres and is
56
- * not covered here (plan note `backup-not-covered:mail`).
64
+ * old dump, or an unreadable list all refuse. `mail` is covered like the rest:
65
+ * Stalwart's data store is a Postgres database.
57
66
  *
58
67
  * Where this Coolify has no executions route, the fallback is that the
59
68
  * schedule exists, is enabled and saves to S3; that proves a dump is
@@ -67,6 +76,13 @@ export declare function requireFreshBackupsForUpgrades(plan: {
67
76
  }, confirmed: readonly string[] | undefined, ports: BackupGatePorts): Promise<{
68
77
  backupId: string | null;
69
78
  }>;
79
+ /**
80
+ * The sign-in database move's gate (#746): the company's recorded `auth` database needs a schedule and a
81
+ * `success` execution not older than {@link BACKUP_MAX_AGE_HOURS} hours, read from Coolify's executions.
82
+ * A schedule alone proves no dump ran, so a Coolify with no executions route refuses too. Returns the
83
+ * uuid of the execution that passed.
84
+ */
85
+ export declare function requireFreshBackupForSigninMove(ports: BackupGatePorts): Promise<string>;
70
86
  export interface MoveGatePorts {
71
87
  /** The company's Coolify project (`coolify_project`). */
72
88
  projectUuid: string | undefined;
@@ -88,6 +104,6 @@ export declare function requireFreshBackupsForMove(ports: MoveGatePorts): Promis
88
104
  /**
89
105
  * The platform databases an upgrade of `service` can migrate. Postgres is all three. archon, ai and
90
106
  * integrations each migrate their own database when the new container starts. The owner company runs its own
91
- * sign-in, files and jobs, so the platform has no database for them; mail, edge and hc have no Postgres.
107
+ * sign-in, files and jobs, so the platform has no database for them; edge and hc have no Postgres.
92
108
  */
93
109
  export declare function platformDatabasesForService(service: string): readonly string[] | 'all';
@@ -44,6 +44,21 @@ export class NetworkMoveBackupMissingError extends Error {
44
44
  this.name = 'NetworkMoveBackupMissingError';
45
45
  }
46
46
  }
47
+ /**
48
+ * The move of a company's sign-in database to the TLS image (#746) is refused before any step runs when
49
+ * the old database has no finished backup from the last {@link BACKUP_MAX_AGE_HOURS} hours, or the host
50
+ * has no backup gate. The message names no database, uuid or URL. There is no override.
51
+ */
52
+ export class SigninDbMoveBackupMissingError extends Error {
53
+ reason;
54
+ constructor(reason) {
55
+ super(reason === 'no-gate'
56
+ ? 'sign-in database move refused: this host has no backup gate, so a fresh backup cannot be checked'
57
+ : `sign-in database move refused: the old sign-in database has no finished backup from the last ${BACKUP_MAX_AGE_HOURS} hours, checked in Coolify; take one now and apply again`);
58
+ this.reason = reason;
59
+ this.name = 'SigninDbMoveBackupMissingError';
60
+ }
61
+ }
47
62
  async function withinTimeout(call) {
48
63
  let timer;
49
64
  try {
@@ -123,8 +138,8 @@ function freshSuccess(executions, now, maxAgeMs = BACKUP_MAX_AGE_HOURS * 3_600_0
123
138
  * `unconfirmedUpgrades` and before any step. A service whose database the
124
139
  * company does not have passes (there is nothing to migrate); a database that
125
140
  * exists with no recorded schedule, a disabled or non-S3 schedule, a failed or
126
- * old dump, or an unreadable list all refuse. `mail` has no Postgres and is
127
- * not covered here (plan note `backup-not-covered:mail`).
141
+ * old dump, or an unreadable list all refuse. `mail` is covered like the rest:
142
+ * Stalwart's data store is a Postgres database.
128
143
  *
129
144
  * Where this Coolify has no executions route, the fallback is that the
130
145
  * schedule exists, is enabled and saves to S3; that proves a dump is
@@ -184,6 +199,31 @@ export async function requireFreshBackupsForUpgrades(plan, confirmed, ports) {
184
199
  }
185
200
  return { backupId };
186
201
  }
202
+ /**
203
+ * The sign-in database move's gate (#746): the company's recorded `auth` database needs a schedule and a
204
+ * `success` execution not older than {@link BACKUP_MAX_AGE_HOURS} hours, read from Coolify's executions.
205
+ * A schedule alone proves no dump ran, so a Coolify with no executions route refuses too. Returns the
206
+ * uuid of the execution that passed.
207
+ */
208
+ export async function requireFreshBackupForSigninMove(ports) {
209
+ const now = (ports.now ?? Date.now)();
210
+ const databaseUuid = ports.recordedIds[companyDatabaseKeys({ id: ports.orgId }).auth];
211
+ const backupUuid = ports.recordedIds[backupScopeKey('auth')];
212
+ if (!databaseUuid || !backupUuid)
213
+ throw new SigninDbMoveBackupMissingError('no-backup');
214
+ let executions;
215
+ try {
216
+ executions = await readExecutions(ports.coolify, databaseUuid, backupUuid);
217
+ }
218
+ catch {
219
+ // An unreadable list is not a fresh backup. The cause stays out: it can carry a response body.
220
+ throw new SigninDbMoveBackupMissingError('no-backup');
221
+ }
222
+ const fresh = executions ? freshSuccess(executions, now) : null;
223
+ if (!fresh)
224
+ throw new SigninDbMoveBackupMissingError('no-backup');
225
+ return fresh.uuid;
226
+ }
187
227
  /**
188
228
  * The gate for a move to the company's own network, which deletes every database of the project. It
189
229
  * fails closed and is stricter than the upgrade gate: it reads the project itself, not foundry's records.
@@ -252,10 +292,15 @@ export async function requireFreshBackupsForMove(ports) {
252
292
  /**
253
293
  * The platform databases an upgrade of `service` can migrate. Postgres is all three. archon, ai and
254
294
  * integrations each migrate their own database when the new container starts. The owner company runs its own
255
- * sign-in, files and jobs, so the platform has no database for them; mail, edge and hc have no Postgres.
295
+ * sign-in, files and jobs, so the platform has no database for them; edge and hc have no Postgres.
256
296
  */
257
297
  export function platformDatabasesForService(service) {
258
298
  if (service === 'postgres')
259
299
  return 'all';
260
- return service === 'archon' || service === 'ai' || service === 'integrations' ? [service] : [];
300
+ return service === 'archon' ||
301
+ service === 'ai' ||
302
+ service === 'integrations' ||
303
+ service === 'mail'
304
+ ? [service]
305
+ : [];
261
306
  }
@@ -11,6 +11,11 @@ import type { DraftStep } from './diff.js';
11
11
  * `app` is the hosting entry's database, recorded as a resource id.
12
12
  */
13
13
  export declare function companyDatabaseKeys(org: Pick<DeclaredOrg, 'id'>): Readonly<Record<string, string>>;
14
+ /**
15
+ * The database a company backup schedule must name when the Coolify resource
16
+ * is not left to pick its first database. Absent: the resource's first database.
17
+ */
18
+ export declare const COMPANY_BACKUP_DATABASE_NAMES: Readonly<Record<string, string>>;
14
19
  /** What every recorded-id key of a company database's backup schedule starts with. */
15
20
  export declare const BACKUP_SCOPE_PREFIX = "backup_";
16
21
  /** The recorded-id key a company database's backup schedule uuid is stored under. */
@@ -22,8 +27,8 @@ export declare function databasesForService(service: string): readonly string[]
22
27
  * recorded uuid and no recorded backup yet. `storageUuid` undefined means the
23
28
  * host did not wire backups, so nothing is planned and nothing is said; null
24
29
  * means the platform's backup storage is not registered, so each database is
25
- * reported as blocked instead (rule 2). A company that runs its own mail
26
- * server carries a note that the Stalwart volume has no Postgres to dump.
30
+ * reported as blocked instead (rule 2). A company's mail server is one of
31
+ * these databases (Stalwart's data store), dumped like the rest.
27
32
  */
28
33
  export declare function planCompanyBackups(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, storageUuid: string | null | undefined): {
29
34
  steps: DraftStep[];
@@ -1,6 +1,8 @@
1
- import { ownsServices } from '../org.js';
2
1
  import { BOULE_DATABASE_SCOPE_KEY } from './boule.js';
3
2
  import { CHAT_REKEY_MAS_DB_SCOPE_KEY, CHAT_REKEY_SYNAPSE_DB_SCOPE_KEY } from './executor.js';
3
+ import { MAIL_DATABASE_NAME, MAIL_DATABASE_SCOPE_KEY } from './mail-store.js';
4
+ import { signinDbBackupMissing } from './signin-db-backup.js';
5
+ import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db-tls.js';
4
6
  /**
5
7
  * F6: the Postgres databases of a company that get a nightly backup, and the
6
8
  * recorded id each one's uuid sits under. The keys are second, documented
@@ -14,6 +16,7 @@ export function companyDatabaseKeys(org) {
14
16
  return {
15
17
  app: `coolify:database:${org.id}`,
16
18
  cms: 'coolify_cms_database',
19
+ mail: MAIL_DATABASE_SCOPE_KEY,
17
20
  auth: 'coolify_platform_auth_database',
18
21
  files: 'coolify_platform_files_database',
19
22
  jobs: 'coolify_jobs_database',
@@ -28,6 +31,13 @@ export function companyDatabaseKeys(org) {
28
31
  chat_rekey_mas: CHAT_REKEY_MAS_DB_SCOPE_KEY,
29
32
  };
30
33
  }
34
+ /**
35
+ * The database a company backup schedule must name when the Coolify resource
36
+ * is not left to pick its first database. Absent: the resource's first database.
37
+ */
38
+ export const COMPANY_BACKUP_DATABASE_NAMES = {
39
+ mail: MAIL_DATABASE_NAME,
40
+ };
31
41
  /** What every recorded-id key of a company database's backup schedule starts with. */
32
42
  export const BACKUP_SCOPE_PREFIX = 'backup_';
33
43
  /** The recorded-id key a company database's backup schedule uuid is stored under. */
@@ -43,6 +53,8 @@ export function databasesForService(service) {
43
53
  return ['files'];
44
54
  case 'jobs':
45
55
  return ['jobs'];
56
+ case 'mail':
57
+ return ['mail'];
46
58
  case 'synapse':
47
59
  return ['synapse', 'chat_synapse'];
48
60
  case 'mas':
@@ -58,8 +70,8 @@ export function databasesForService(service) {
58
70
  * recorded uuid and no recorded backup yet. `storageUuid` undefined means the
59
71
  * host did not wire backups, so nothing is planned and nothing is said; null
60
72
  * means the platform's backup storage is not registered, so each database is
61
- * reported as blocked instead (rule 2). A company that runs its own mail
62
- * server carries a note that the Stalwart volume has no Postgres to dump.
73
+ * reported as blocked instead (rule 2). A company's mail server is one of
74
+ * these databases (Stalwart's data store), dumped like the rest.
63
75
  */
64
76
  export function planCompanyBackups(org, recordedIds, storageUuid) {
65
77
  const steps = [];
@@ -91,10 +103,17 @@ export function planCompanyBackups(org, recordedIds, storageUuid) {
91
103
  },
92
104
  });
93
105
  }
94
- if (ownsServices(org)) {
106
+ const oldDatabase = recordedIds[companyDatabaseKeys(org).auth];
107
+ if (recordedIds[SIGNIN_DB_TLS_SCOPE_KEY] && oldDatabase) {
108
+ blocked.push({
109
+ description: `${org.slug}'s old Coolify sign-in database (${oldDatabase}) is no longer used; delete it by hand in Coolify when you no longer want it as a fallback, foundry never deletes it`,
110
+ key: 'signin-db-old-database',
111
+ });
112
+ }
113
+ if (signinDbBackupMissing(recordedIds)) {
95
114
  blocked.push({
96
- description: `${org.slug}'s mail server keeps its data on a volume, not in Postgres: no backup covers it yet`,
97
- key: 'backup-not-covered:mail',
115
+ description: `${org.slug}'s sign-in database runs as foundry's own image, which Coolify's database backups do not cover: its nightly dump is not scheduled yet, so no backup covers it`,
116
+ key: 'backup-not-covered:signin-db',
98
117
  });
99
118
  }
100
119
  return { steps, blocked };
@@ -69,6 +69,10 @@ export interface PlanOrgOptions {
69
69
  ownMailKeyHeld?: boolean;
70
70
  /** The executor can mint an own org's Lettermint key (`FoundryStepExecutor.ownMailKeyMintable`). */
71
71
  ownMailKeyMintable?: boolean;
72
+ /** The executor can store the sign-in database's CA and certificate (`FoundryStepExecutor.ownSigninDbKeysPuttable`). Absent: true. */
73
+ ownSigninDbKeysPuttable?: boolean;
74
+ /** The host supplies the backup bucket the sign-in database dumps into (`FoundryStepExecutor.ownSigninDbBackupConfigured`). Absent: true. */
75
+ ownSigninDbBackupConfigured?: boolean;
72
76
  /** The own jobs admin token's held-key ref holds a value. */
73
77
  ownJobsTokenHeld?: boolean;
74
78
  /** The own media server's key pair ref holds a value. */
package/dist/plan/diff.js CHANGED
@@ -14,8 +14,9 @@ import { appHostname, tunnelDnsHostnames } from './hostnames.js';
14
14
  import { lockedHostnames } from './locked-hostnames.js';
15
15
  import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
16
16
  import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
17
+ import { mailStoreSwitchChange } from './mail-store.js';
17
18
  import { hasAdoptedResources, networkMoveNeeded, planNetworkMove } from './network-move.js';
18
- import { OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, planOwnSfu, } from './own-diff.js';
19
+ import { OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, OWN_SIGNIN_DB_MOVE_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, planOwnSfu, planSigninDbMove, } from './own-diff.js';
19
20
  import { ownDnsHostnames } from './own-dns.js';
20
21
  import { ownHostnames } from './own-hostnames.js';
21
22
  import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
@@ -196,9 +197,20 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
196
197
  oikosZoneReady: options.oikosZoneReady ?? false,
197
198
  mailKeyHeld: options.ownMailKeyHeld ?? false,
198
199
  mailKeyMintable: options.ownMailKeyMintable ?? false,
200
+ now: (options.now ?? Date.now)(),
201
+ signinDbKeysPuttable: options.ownSigninDbKeysPuttable ?? true,
202
+ signinDbBackupConfigured: options.ownSigninDbBackupConfigured ?? true,
199
203
  });
200
204
  if (ownAuth.step)
201
205
  plannedSteps.push(ownAuth.step);
206
+ const signinDbMove = planSigninDbMove({
207
+ org,
208
+ recordedIds,
209
+ signinDbKeysPuttable: options.ownSigninDbKeysPuttable ?? true,
210
+ signinDbBackupConfigured: options.ownSigninDbBackupConfigured ?? true,
211
+ });
212
+ if (signinDbMove.step)
213
+ plannedSteps.push(signinDbMove.step);
202
214
  // An own org's ZITADEL is the company's own instance: nothing that needs it plans before it exists.
203
215
  const ownSignInPending = ownsServices(org) && !ownAuthApplied(org, recordedIds);
204
216
  const issuer = planIssuer(org, byKey, claimed, platformReadiness.authReady);
@@ -319,6 +331,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
319
331
  // uses, never `versionChangeLine`'s "X -> Y (redeploy)" -- nothing here
320
332
  // ever performs that redeploy.
321
333
  const versionChanges = [];
334
+ if (ownMail.step?.action.desired.switchMailStore) {
335
+ versionChanges.push(mailStoreSwitchChange(org.slug));
336
+ }
322
337
  if (org.calls.enabled && !calls.blocked) {
323
338
  const callsAppLive = Boolean(byKey.get(resourceKey('coolify', 'calls', org.id)));
324
339
  const synapseChange = versionNotAppliedLine('synapse', callsAppLive, recordedIds[CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY], versionPin?.calls ?? VERSIONS.synapse);
@@ -606,6 +621,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
606
621
  unmanaged.push(...edgeReservedLabels.blocked);
607
622
  if (ownAuth.blocked)
608
623
  unmanaged.push({ description: ownAuth.blocked, key: OWN_AUTH_BLOCKED_KEY });
624
+ if (signinDbMove.blocked) {
625
+ unmanaged.push({ description: signinDbMove.blocked, key: OWN_SIGNIN_DB_MOVE_BLOCKED_KEY });
626
+ }
609
627
  if (ownJobs.blocked)
610
628
  unmanaged.push({ description: ownJobs.blocked, key: OWN_JOBS_BLOCKED_KEY });
611
629
  if (ownMail.blocked)
@@ -325,6 +325,8 @@ export interface CoolifyPort {
325
325
  description?: string;
326
326
  instant_deploy?: boolean;
327
327
  custom_docker_run_options?: string;
328
+ /** `false`: no domain is made for the app. Coolify makes one by default, which would route to the app's port. */
329
+ autogenerate_domain?: boolean;
328
330
  }): Promise<{
329
331
  uuid: string;
330
332
  }>;
@@ -1828,6 +1830,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1828
1830
  private executeScoped;
1829
1831
  /** Whether `ensureOwnAuthMailToken` can mint: the same two inputs it blocks on. */
1830
1832
  ownMailKeyMintable(): boolean;
1833
+ /** Whether the held keys can store a new company's sign-in database CA and certificate (#746). */
1834
+ ownSigninDbKeysPuttable(): boolean;
1835
+ /** Whether the host supplies the backup bucket the sign-in database dumps into (#746). */
1836
+ ownSigninDbBackupConfigured(): boolean;
1831
1837
  /** What the executor's own files dependencies supply, for the planner's blocked lines. */
1832
1838
  ownFilesDeps(): {
1833
1839
  keys: boolean;
@@ -2927,6 +2933,13 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2927
2933
  * declared auth ref patches both git apps' branch and redeploys them; the
2928
2934
  * applied marker comes last, so a run that died halfway is planned again.
2929
2935
  */
2936
+ /**
2937
+ * The sign-in ports of one company's steps: its own network and limits, a SMTP target without the
2938
+ * platform's password, and every secret the step reads registered for redaction.
2939
+ */
2940
+ private ownAuthDeps;
2941
+ /** `plan/signin-db-move.ts`: copies a company's sign-in data from its Coolify database to the TLS database. */
2942
+ private ownAuthDbMove;
2930
2943
  private ownAuth;
2931
2944
  /**
2932
2945
  * `plan/own-diff.ts`'s `planOwnMail`: the org's own Stalwart in the org's own
@@ -13,7 +13,7 @@ import { recordBuiltCommit } from './build-record.js';
13
13
  import { MAS_TAG, SYNAPSE_IMAGE, SYNAPSE_TAG, callsMasConfigYaml, callsServerName, callsSynapseHomeserverYaml, masDockerfile, mintCallsMatrixSecrets, parseCallsDbUrl, } from './calls-matrix.js';
14
14
  import { CHAT_MAS_PORT, CHAT_REPO, CHAT_SERVICE_DOCKERFILE, CHAT_SERVICE_PORT, CHAT_SYNAPSE_DOCKERFILE, CHAT_SYNAPSE_PORT, assertChatServerName, chatInternalNames, chatMasConfigYaml, chatRekeyedInternalNames, chatServerName, chatServiceEnvs, chatSynapseHomeserverYaml, chatUpstreams, } from './chat-matrix.js';
15
15
  import { ensureCloudflaredConnector } from './cloudflared-connector.js';
16
- import { BACKUP_SCOPE_PREFIX, backupScopeKey } from './company-backups.js';
16
+ import { BACKUP_SCOPE_PREFIX, COMPANY_BACKUP_DATABASE_NAMES, backupScopeKey, } from './company-backups.js';
17
17
  import { CORE_FILES_COLLECTION_NAME, FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY, } from './core-files.js';
18
18
  import { waitForNewDeployment, } from './deploy-health.js';
19
19
  import { PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, onPlatformNetwork, placed, placedByScope, } from './destinations.js';
@@ -44,6 +44,7 @@ import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_
44
44
  import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_MAIL_SERVER_SCOPE_KEY, defaultStalwartForInstance, filesDepsInvalid, filesShareDomains, provisionAuthStack, provisionDatabaseBackup, provisionFilesHost, provisionJobsHost, provisionMailServer, resolveOwnFilesKeys, } from './platform-providers.js';
45
45
  import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, ENTRY_CONTAINER_LABELS, appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryProxyLabels, entryRoutes, entryRoutesMarker, ownsOikosName, routesWithoutToken, sameLabelSets, } from './proxy-labels.js';
46
46
  import { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, closeSignInOrgRegistration, ensureSignInOrg, inviteOwnerWithLink, registerSignInDomainOrBlock, signInOrgDomain, } from './sign-in-org.js';
47
+ import { moveSigninDatabase } from './signin-db-move.js';
47
48
  import { entryWorkerAppliedScopeKey, entryWorkerExclusionsScopeKey, entryWorkerRetiredScopeKey, entryWorkerRouteIdScopeKey, entryWorkerRoutePatternScopeKey, entryWorkerRoutePendingScopeKey, entryWorkerRouteZoneScopeKey, entryWorkerUploadedScopeKey, } from './worker.js';
48
49
  /**
49
50
  * The real {@link StepExecutor} (#20 "provision Company Factory end to
@@ -940,6 +941,15 @@ export class FoundryStepExecutor {
940
941
  ownMailKeyMintable() {
941
942
  return Boolean(this.ports.lettermint && this.ports.ownServices?.provision.auth.heldKeys);
942
943
  }
944
+ /** Whether the held keys can store a new company's sign-in database CA and certificate (#746). */
945
+ ownSigninDbKeysPuttable() {
946
+ return Boolean(this.ports.ownServices?.provision.auth.heldKeys?.put);
947
+ }
948
+ /** Whether the host supplies the backup bucket the sign-in database dumps into (#746). */
949
+ ownSigninDbBackupConfigured() {
950
+ const files = this.ports.ownServices?.provision.files;
951
+ return Boolean(files?.r2Buckets && files.r2Keys);
952
+ }
943
953
  /** What the executor's own files dependencies supply, for the planner's blocked lines. */
944
954
  ownFilesDeps() {
945
955
  const files = this.ports.ownServices?.provision.files;
@@ -1132,6 +1142,8 @@ export class FoundryStepExecutor {
1132
1142
  return this.jobsRegistration(action, org, context);
1133
1143
  case 'coolify:own-auth':
1134
1144
  return this.ownAuth(action, org, context);
1145
+ case 'coolify:own-auth-db-move':
1146
+ return this.ownAuthDbMove(org, context);
1135
1147
  case 'coolify:own-jobs':
1136
1148
  return this.ownJobs(action, org, context);
1137
1149
  case 'coolify:own-mail':
@@ -2633,6 +2645,7 @@ export class FoundryStepExecutor {
2633
2645
  databaseUuid: databaseId,
2634
2646
  label: `${org.slug} ${database}`,
2635
2647
  storageUuid,
2648
+ databaseName: COMPANY_BACKUP_DATABASE_NAMES[database],
2636
2649
  });
2637
2650
  context.recordScopeId(backupScopeKey(database), uuid);
2638
2651
  return undefined;
@@ -5708,30 +5721,55 @@ export class FoundryStepExecutor {
5708
5721
  * declared auth ref patches both git apps' branch and redeploys them; the
5709
5722
  * applied marker comes last, so a run that died halfway is planned again.
5710
5723
  */
5711
- async ownAuth(action, org, context) {
5712
- const own = this.ports.ownServices;
5713
- if (!own)
5714
- throw new Error(OWN_SERVICES_PORT_ABSENT);
5715
- const hostname = action.desired.hostname;
5716
- const gitRef = action.desired.gitRef;
5717
- const hn = ownHostnames(org);
5718
- const scope = scopeFromOrg(context, {});
5724
+ /**
5725
+ * The sign-in ports of one company's steps: its own network and limits, a SMTP target without the
5726
+ * platform's password, and every secret the step reads registered for redaction.
5727
+ */
5728
+ ownAuthDeps(auth, org, context) {
5719
5729
  // The target (host, port, tls, user) is not secret; the platform's SMTP
5720
5730
  // password is, so the company's own token is the password instead.
5721
- const { smtp, ...authDeps } = own.provision.auth;
5722
- const deps = {
5731
+ const { smtp, signinDbBackup: _platformBackup, ...authDeps } = auth;
5732
+ const files = this.ports.ownServices?.provision.files;
5733
+ return {
5723
5734
  ...authDeps,
5735
+ // The company's own bucket and key, through the files host's ports; never a platform-wide backup key.
5736
+ signinDbBackup: files?.r2Buckets && files.r2Keys
5737
+ ? { r2Buckets: files.r2Buckets, r2Keys: files.r2Keys }
5738
+ : undefined,
5724
5739
  ...(smtp
5725
5740
  ? { smtp: { host: smtp.host, port: smtp.port, tls: smtp.tls, user: smtp.user } }
5726
5741
  : {}),
5727
- coolify: limited(this.placedForCompany(own.provision.auth.coolify, org, context), LIMITS.ownAuth),
5742
+ coolify: limited(this.placedForCompany(auth.coolify, org, context), LIMITS.ownAuth),
5728
5743
  runtime: {
5729
- ...own.provision.auth.runtime,
5744
+ ...auth.runtime,
5730
5745
  onSecret: (value) => {
5731
5746
  this.handledSecrets.add(value);
5732
5747
  },
5733
5748
  },
5734
5749
  };
5750
+ }
5751
+ /** `plan/signin-db-move.ts`: copies a company's sign-in data from its Coolify database to the TLS database. */
5752
+ async ownAuthDbMove(org, context) {
5753
+ const own = this.ports.ownServices;
5754
+ if (!own)
5755
+ throw new Error(OWN_SERVICES_PORT_ABSENT);
5756
+ const deps = this.ownAuthDeps(own.provision.auth, org, context);
5757
+ const placed = this.placedForCompany(own.provision.auth.coolify, org, context);
5758
+ await moveSigninDatabase(scopeFromOrg(context, {}), { ...deps, readOldDatabase: (uuid) => placed.getDatabase(uuid) }, orgTarget(org), {
5759
+ project: { ensure: () => this.ensureCoolifyProject(org, context) },
5760
+ label: `${org.slug}:auth`,
5761
+ });
5762
+ return {};
5763
+ }
5764
+ async ownAuth(action, org, context) {
5765
+ const own = this.ports.ownServices;
5766
+ if (!own)
5767
+ throw new Error(OWN_SERVICES_PORT_ABSENT);
5768
+ const hostname = action.desired.hostname;
5769
+ const gitRef = action.desired.gitRef;
5770
+ const hn = ownHostnames(org);
5771
+ const scope = scopeFromOrg(context, {});
5772
+ const deps = this.ownAuthDeps(own.provision.auth, org, context);
5735
5773
  const before = scope.recordedIds();
5736
5774
  const refChanged = before[OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY] !== undefined &&
5737
5775
  before[OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY] !== gitRef;
@@ -5813,12 +5851,17 @@ export class FoundryStepExecutor {
5813
5851
  },
5814
5852
  zitadel: own.zitadelFor({ issuerUrl, bearerRef: target.refs.instanceBearer }),
5815
5853
  zitadelForInstance: undefined,
5854
+ runtime: {
5855
+ ...own.provision.mail.runtime,
5856
+ onSecret: (value) => this.handledSecrets.add(value),
5857
+ },
5816
5858
  }, target, {
5817
5859
  hostname: action.desired.hostname,
5818
5860
  apex: hn.mailDomain,
5819
5861
  zone: hn.zone,
5820
5862
  mailHost: action.desired.mailHost,
5821
5863
  imageTag: action.desired.gitRef,
5864
+ switchStore: action.desired.switchMailStore === true,
5822
5865
  hostPorts: bind ? `${bind}:${smtpPort}:25` : `${smtpPort}:25`,
5823
5866
  issuerUrl,
5824
5867
  mxRecord: null,
@@ -23,6 +23,7 @@ export { AdoptRefusedError } from './adopt.js';
23
23
  export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, } from './plan-apply.js';
24
24
  export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
25
25
  export type { UnconfirmedUpgrade } from './confirm-upgrade.js';
26
+ export { MAIL_STORE_SWITCH_SERVICE } from './mail-store.js';
26
27
  export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './backup-gate.js';
27
28
  export type { BackupGatePorts, MoveGatePorts } from './backup-gate.js';
28
29
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, provisionFilesHost, provisionJobsHost, provisionMailServer, } from './platform-providers.js';
@@ -63,7 +64,7 @@ export { createEdgeAccess } from './edge-access.js';
63
64
  export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, EdgePlatformAccess, } from './edge-access.js';
64
65
  export { createEdgeWorkers } from './edge-workers.js';
65
66
  export type { EdgePlatformWorkers, EdgeWorkersOptions, PlatformWorkersPort, } from './edge-workers.js';
66
- export type { PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, } from './uptime-worker.js';
67
+ export type { PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeChatDeps, PlatformUptimeDeps, PlatformWorkersFactory, } from './uptime-worker.js';
67
68
  export { createEdgeRateLimits } from './edge-ratelimits.js';
68
69
  export type { EdgePlatformRateLimits, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, } from './edge-ratelimits.js';
69
70
  export { createEdgeZones } from './edge-zones.js';
@@ -10,6 +10,7 @@ export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICE
10
10
  export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
11
11
  export { AdoptRefusedError } from './adopt.js';
12
12
  export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
13
+ export { MAIL_STORE_SWITCH_SERVICE } from './mail-store.js';
13
14
  export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './backup-gate.js';
14
15
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, provisionFilesHost, provisionJobsHost, provisionMailServer, } from './platform-providers.js';
15
16
  // #171: how a host signs files management requests with the delivered key.
@@ -0,0 +1,34 @@
1
+ import { type VersionChangeItem } from '../plan.js';
2
+ /**
3
+ * Stalwart keeps its data (mail, accounts, settings, DKIM keys) in a Coolify
4
+ * Postgres, so the nightly dump of that database is the off-server backup.
5
+ * These keys are shared by the planners (which must not import
6
+ * `platform-providers.ts`) and by `provisionMailServer`.
7
+ */
8
+ /** The recorded uuid of the mail Postgres, under the same name for the platform and every company. */
9
+ export declare const MAIL_DATABASE_SCOPE_KEY = "coolify_mail_database";
10
+ /** Recorded as {@link MAIL_STORE_POSTGRES} once the `Bootstrap` write has pointed Stalwart at the database. */
11
+ export declare const MAIL_STORE_SCOPE_KEY = "mail_store";
12
+ export declare const MAIL_STORE_POSTGRES = "postgres";
13
+ /** The recorded uuid of the Stalwart app (`platform-providers.ts` re-exports it under its old name). */
14
+ export declare const MAIL_APP_SCOPE_KEY = "coolify_platform_mail_app";
15
+ /** The database and its owner, and the env var Stalwart reads the password from. */
16
+ export declare const MAIL_DATABASE_NAME = "stalwart";
17
+ export declare const MAIL_DATABASE_USER = "stalwart";
18
+ export declare const MAIL_DATABASE_PASSWORD_ENV = "STALWART_DB_PASSWORD";
19
+ /** The `confirmUpgrade` name that confirms the switch (`VersionChangeItem.service`). */
20
+ export declare const MAIL_STORE_SWITCH_SERVICE = "mail-store";
21
+ /**
22
+ * A Stalwart app is recorded and its data store was never moved to Postgres
23
+ * (no mail database recorded either: a fresh instance records that before it
24
+ * creates the app, so a crash halfway never reads as a volume-backed server).
25
+ */
26
+ export declare function mailStoreIsLegacy(recorded: Readonly<Record<string, string>>): boolean;
27
+ /** The one step description for the switch. `owner` is `the platform` or the company's slug. */
28
+ export declare function mailStoreSwitchDescription(owner: string): string;
29
+ /**
30
+ * The version-change line that makes `apply` refuse the plan until
31
+ * `confirmUpgrade` names `mail-store`. Fixed text: it is part of the
32
+ * stale-plan comparison.
33
+ */
34
+ export declare function mailStoreSwitchChange(owner: string): VersionChangeItem;
@@ -0,0 +1,49 @@
1
+ import { VERSION_CHANGE_KIND } from '../plan.js';
2
+ /**
3
+ * Stalwart keeps its data (mail, accounts, settings, DKIM keys) in a Coolify
4
+ * Postgres, so the nightly dump of that database is the off-server backup.
5
+ * These keys are shared by the planners (which must not import
6
+ * `platform-providers.ts`) and by `provisionMailServer`.
7
+ */
8
+ /** The recorded uuid of the mail Postgres, under the same name for the platform and every company. */
9
+ export const MAIL_DATABASE_SCOPE_KEY = 'coolify_mail_database';
10
+ /** Recorded as {@link MAIL_STORE_POSTGRES} once the `Bootstrap` write has pointed Stalwart at the database. */
11
+ export const MAIL_STORE_SCOPE_KEY = 'mail_store';
12
+ export const MAIL_STORE_POSTGRES = 'postgres';
13
+ /** The recorded uuid of the Stalwart app (`platform-providers.ts` re-exports it under its old name). */
14
+ export const MAIL_APP_SCOPE_KEY = 'coolify_platform_mail_app';
15
+ /** The database and its owner, and the env var Stalwart reads the password from. */
16
+ export const MAIL_DATABASE_NAME = 'stalwart';
17
+ export const MAIL_DATABASE_USER = 'stalwart';
18
+ export const MAIL_DATABASE_PASSWORD_ENV = 'STALWART_DB_PASSWORD';
19
+ /** The `confirmUpgrade` name that confirms the switch (`VersionChangeItem.service`). */
20
+ export const MAIL_STORE_SWITCH_SERVICE = 'mail-store';
21
+ /**
22
+ * A Stalwart app is recorded and its data store was never moved to Postgres
23
+ * (no mail database recorded either: a fresh instance records that before it
24
+ * creates the app, so a crash halfway never reads as a volume-backed server).
25
+ */
26
+ export function mailStoreIsLegacy(recorded) {
27
+ return (Boolean(recorded[MAIL_APP_SCOPE_KEY]) &&
28
+ !recorded[MAIL_DATABASE_SCOPE_KEY] &&
29
+ recorded[MAIL_STORE_SCOPE_KEY] !== MAIL_STORE_POSTGRES);
30
+ }
31
+ /** The one step description for the switch. `owner` is `the platform` or the company's slug. */
32
+ export function mailStoreSwitchDescription(owner) {
33
+ return `switch ${owner}'s mail store to Postgres (recreates the mail server; mail stored there is deleted)`;
34
+ }
35
+ /**
36
+ * The version-change line that makes `apply` refuse the plan until
37
+ * `confirmUpgrade` names `mail-store`. Fixed text: it is part of the
38
+ * stale-plan comparison.
39
+ */
40
+ export function mailStoreSwitchChange(owner) {
41
+ return {
42
+ kind: VERSION_CHANGE_KIND,
43
+ service: MAIL_STORE_SWITCH_SERVICE,
44
+ description: `${owner}'s mail server still stores its data on a volume: moving it to Postgres recreates the server and deletes the mail stored there; confirm with confirmUpgrade "${MAIL_STORE_SWITCH_SERVICE}"`,
45
+ from: 'volume',
46
+ to: 'postgres',
47
+ needsConfirmation: true,
48
+ };
49
+ }