@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
|
@@ -17,11 +17,14 @@ import { LIMITS, limited } from './own-limits.js';
|
|
|
17
17
|
export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
18
18
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
19
19
|
import { MAIL_RELAY_BLOCKED, provisionMailRelay } from './mail-relay.js';
|
|
20
|
+
import { MAIL_APP_SCOPE_KEY, MAIL_DATABASE_NAME, MAIL_DATABASE_PASSWORD_ENV, MAIL_DATABASE_SCOPE_KEY, MAIL_STORE_POSTGRES, MAIL_STORE_SCOPE_KEY, mailStoreIsLegacy, } from './mail-store.js';
|
|
20
21
|
import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
|
|
21
22
|
import { recordOwnDnsHostname } from './own-dns.js';
|
|
22
23
|
import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
|
|
23
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_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_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_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_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_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_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
24
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_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_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_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_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_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_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
24
25
|
import { ensureSignInOrg, inviteOwnerWithLink, signInOrgDomain } from './sign-in-org.js';
|
|
26
|
+
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY } from './signin-db-tls.js';
|
|
27
|
+
import { SIGNIN_DB_APP_SCOPE_KEY, SIGNIN_DB_TLS_SCOPE_KEY, dockerfileAppCreator, ensureSigninDbCaFile, provisionSigninDb, waitHealthy, withCallTimeout, } from './signin-db.js';
|
|
25
28
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
26
29
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
27
30
|
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
@@ -372,7 +375,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
372
375
|
if (!apiKey)
|
|
373
376
|
blocked.heartbeatsApiKeyMissing = true;
|
|
374
377
|
}
|
|
375
|
-
await this.uptimeBlocked(platform, recordedIds, blocked);
|
|
378
|
+
await this.uptimeBlocked(platform, recordedIds, blocked, owner);
|
|
376
379
|
// #604: only once edge and sign-in are applied (the planner reports
|
|
377
380
|
// that case itself) and no provider is recorded yet.
|
|
378
381
|
if (platform.edge.enabled &&
|
|
@@ -440,7 +443,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
440
443
|
* checks the pin and Archon first, so none of the reads here happens before
|
|
441
444
|
* they hold.
|
|
442
445
|
*/
|
|
443
|
-
async uptimeBlocked(platform, recordedIds, blocked) {
|
|
446
|
+
async uptimeBlocked(platform, recordedIds, blocked, owner = {}) {
|
|
444
447
|
const deployed = Boolean(recordedIds[`cloudflare:worker:${PLATFORM_UPTIME_KEY}`]) &&
|
|
445
448
|
!recordedIds[PLATFORM_UPTIME_REMOVED_SCOPE_KEY];
|
|
446
449
|
if (!platform.uptime.enabled && !uptimeRecorded(recordedIds))
|
|
@@ -453,15 +456,41 @@ export class FoundryPlatformStepExecutor {
|
|
|
453
456
|
}
|
|
454
457
|
if (!platform.uptime.enabled)
|
|
455
458
|
return;
|
|
456
|
-
|
|
459
|
+
const heldValues = await heldUptimeValues(deps);
|
|
460
|
+
blocked.uptimeSecretsHeld = Object.keys(heldValues);
|
|
457
461
|
const accessTokenId = recordedIds[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY];
|
|
458
462
|
const accessHeld = Boolean((await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF)) &&
|
|
459
463
|
(await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF)));
|
|
460
464
|
blocked.uptimeHeld = {
|
|
461
465
|
token: Boolean(await deps.heldKeys.resolve(PLATFORM_UPTIME_TOKEN_REF)),
|
|
462
466
|
access: accessHeld,
|
|
463
|
-
vars: uptimeVarsDigest(deps.vars
|
|
467
|
+
vars: uptimeVarsDigest(uptimeEffectiveVars(deps.vars, deps.chat && owner.chat && recordedIds[PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY]
|
|
468
|
+
? {
|
|
469
|
+
homeserverUrl: `https://${owner.chat.agoraHost}`,
|
|
470
|
+
roomId: recordedIds[PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY],
|
|
471
|
+
}
|
|
472
|
+
: null)),
|
|
464
473
|
};
|
|
474
|
+
if (deps.chat && !heldValues.CHAT_ACCESS_TOKEN) {
|
|
475
|
+
if (!owner.chat) {
|
|
476
|
+
blocked.uptimeChat = {
|
|
477
|
+
pending: false,
|
|
478
|
+
blocked: "the uptime Worker's chat alerts are blocked, not planned: the owner company's chat is not applied yet",
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
else if (owner.chat.subjects.length === 0) {
|
|
482
|
+
blocked.uptimeChat = {
|
|
483
|
+
pending: false,
|
|
484
|
+
blocked: "the uptime Worker's chat alerts are blocked, not planned: the owner company has no owner with a recorded sign-in user to join the alerts room",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
blocked.uptimeChat = { pending: true };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else if (deps.chat) {
|
|
492
|
+
blocked.uptimeChat = { pending: false };
|
|
493
|
+
}
|
|
465
494
|
const edgeUrl = recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
466
495
|
// A token of the name with no id recorded cannot be minted over, and
|
|
467
496
|
// foundry does not remove what it did not make. With an id recorded the
|
|
@@ -667,7 +696,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
667
696
|
if (!this.mail) {
|
|
668
697
|
throw new Error('platform:mail needs FoundryPlatformStepExecutor to be constructed with its mail dependencies (#169)');
|
|
669
698
|
}
|
|
670
|
-
return provisionPlatformMail(context, this.limitedOnPlatform(this.mail, LIMITS.platformMail));
|
|
699
|
+
return provisionPlatformMail(context, this.limitedOnPlatform(this.mail, LIMITS.platformMail), action.desired.switchMailStore === true);
|
|
671
700
|
}
|
|
672
701
|
if (action.system === 'coolify' &&
|
|
673
702
|
action.kind === 'app' &&
|
|
@@ -1067,7 +1096,7 @@ export const AUTH_ZITADEL_PATH_PATTERN = '^/(oidc|oauth|saml|management|admin|sy
|
|
|
1067
1096
|
*/
|
|
1068
1097
|
const AUTH_BROKER_PATH_PATTERN = '^/api(/|$)';
|
|
1069
1098
|
const PLATFORM_AUTH_PROJECT_SCOPE_KEY = 'coolify_project_platform_auth';
|
|
1070
|
-
const PLATFORM_AUTH_DB_SCOPE_KEY = 'coolify_platform_auth_database';
|
|
1099
|
+
export const PLATFORM_AUTH_DB_SCOPE_KEY = 'coolify_platform_auth_database';
|
|
1071
1100
|
export const PLATFORM_AUTH_ZITADEL_SCOPE_KEY = 'coolify_platform_auth_zitadel_app';
|
|
1072
1101
|
export const PLATFORM_AUTH_LOGIN_SCOPE_KEY = 'coolify_platform_auth_login_app';
|
|
1073
1102
|
const PLATFORM_AUTH_TUNNEL_SCOPE_KEY = 'cloudflare_platform_auth_tunnel';
|
|
@@ -1402,33 +1431,70 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1402
1431
|
const projectUuid = await input.project.ensure(deps.coolify, scope);
|
|
1403
1432
|
const { environment } = await deps.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
1404
1433
|
// ---- database ----------------------------------------------------------
|
|
1405
|
-
|
|
1434
|
+
// A company's sign-in database is foundry's own TLS image (#746). A company still on the Coolify
|
|
1435
|
+
// database keeps it until its move step has run (`signin-db-move.ts`); the platform's stays on it.
|
|
1436
|
+
const legacyDbId = recorded[PLATFORM_AUTH_DB_SCOPE_KEY];
|
|
1437
|
+
const tlsDatabase = target.kind === 'org' && Boolean(recorded[SIGNIN_DB_TLS_SCOPE_KEY] || !legacyDbId);
|
|
1438
|
+
let dbId = legacyDbId;
|
|
1406
1439
|
// A database ZITADEL has already run against may hold data encrypted with a
|
|
1407
1440
|
// masterkey this run cannot see (see `ensureMasterkey`). A database that
|
|
1408
1441
|
// exists but was never deployed against (a throw between its create and the
|
|
1409
1442
|
// first deploy) is still empty, so a retry may generate the key.
|
|
1410
|
-
const databaseInUse = Boolean(
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1443
|
+
const databaseInUse = Boolean((tlsDatabase ? recorded[SIGNIN_DB_APP_SCOPE_KEY] : dbId) &&
|
|
1444
|
+
recorded[PLATFORM_AUTH_ZITADEL_DEPLOYED_SCOPE_KEY]);
|
|
1445
|
+
let databaseUrl;
|
|
1446
|
+
let signinDbCa;
|
|
1447
|
+
if (tlsDatabase) {
|
|
1448
|
+
const provisioned = await provisionSigninDb({
|
|
1449
|
+
coolify: deps.coolify,
|
|
1450
|
+
keys: deps.heldKeys,
|
|
1451
|
+
runtime,
|
|
1452
|
+
name: target.names.signinDb,
|
|
1453
|
+
internalHost: target.internal.signinDb,
|
|
1454
|
+
volumeName: `${target.names.signinDb}-data`,
|
|
1455
|
+
serverCertRef: target.refs.signinDbServer,
|
|
1456
|
+
slug: target.id,
|
|
1457
|
+
backup: deps.signinDbBackup,
|
|
1458
|
+
backupBucket: target.names.signinBackupBucket,
|
|
1459
|
+
recorded,
|
|
1460
|
+
label,
|
|
1461
|
+
environment,
|
|
1462
|
+
record,
|
|
1463
|
+
create: dockerfileAppCreator(deps.coolify, {
|
|
1464
|
+
projectUuid,
|
|
1465
|
+
environment,
|
|
1466
|
+
name: target.names.signinDb,
|
|
1467
|
+
description: `${target.id}'s sign-in database with TLS (#746)`,
|
|
1468
|
+
}),
|
|
1421
1469
|
});
|
|
1422
|
-
|
|
1423
|
-
|
|
1470
|
+
databaseUrl = provisioned.dsn;
|
|
1471
|
+
signinDbCa = provisioned.ca;
|
|
1472
|
+
await record(SIGNIN_DB_TLS_SCOPE_KEY, provisioned.appId);
|
|
1424
1473
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1474
|
+
else {
|
|
1475
|
+
if (!dbId) {
|
|
1476
|
+
const server = await resolvePlatformServer(deps.coolify);
|
|
1477
|
+
const created = await deps.coolify.createPostgresDatabase({
|
|
1478
|
+
project_uuid: projectUuid,
|
|
1479
|
+
server_uuid: server.uuid,
|
|
1480
|
+
environment_name: environment.name,
|
|
1481
|
+
environment_uuid: environment.uuid,
|
|
1482
|
+
name: target.names.authDb,
|
|
1483
|
+
postgres_db: 'zitadel',
|
|
1484
|
+
postgres_user: 'zitadel',
|
|
1485
|
+
});
|
|
1486
|
+
dbId = created.uuid;
|
|
1487
|
+
await record(PLATFORM_AUTH_DB_SCOPE_KEY, dbId);
|
|
1488
|
+
}
|
|
1489
|
+
const db = await getDatabaseStarted(deps.coolify, dbId);
|
|
1490
|
+
if (!db.internal_db_url) {
|
|
1491
|
+
// Static config check before either app is touched (`diff.ts`'s
|
|
1492
|
+
// `coolifyCalls` follows the same rule for its own databases): Coolify
|
|
1493
|
+
// can take a moment to report a freshly created database's
|
|
1494
|
+
// `internal_db_url`, so the next apply retries from a clean state.
|
|
1495
|
+
throw new Error(`coolify:app for ${subject} (auth): the database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
1496
|
+
}
|
|
1497
|
+
databaseUrl = db.internal_db_url;
|
|
1432
1498
|
}
|
|
1433
1499
|
// ---- the ZITADEL app itself ---------------------------------------------
|
|
1434
1500
|
const zitadelApp = await findOrCreatePlatformApp(deps.coolify, target.names.authZitadel, environment, async () => {
|
|
@@ -1446,6 +1512,8 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1446
1512
|
});
|
|
1447
1513
|
const zitadelAppId = zitadelApp.appId;
|
|
1448
1514
|
await record(PLATFORM_AUTH_ZITADEL_SCOPE_KEY, zitadelAppId);
|
|
1515
|
+
if (signinDbCa)
|
|
1516
|
+
await ensureSigninDbCaFile(deps.coolify, zitadelAppId, signinDbCa, label);
|
|
1449
1517
|
// Set on every run, not only after a create: a throw right after the
|
|
1450
1518
|
// create must not leave the app unnamed for good, and the tunnel addresses
|
|
1451
1519
|
// the app by this name. (The port cannot read the current name back, and
|
|
@@ -1548,7 +1616,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1548
1616
|
{ key: 'ZITADEL_EXTERNALPORT', value: '443' },
|
|
1549
1617
|
{ key: 'ZITADEL_EXTERNALSECURE', value: 'true' },
|
|
1550
1618
|
{ key: 'ZITADEL_TLS_ENABLED', value: 'false' },
|
|
1551
|
-
{ key: 'ZITADEL_DATABASE_POSTGRES_DSN', value:
|
|
1619
|
+
{ key: 'ZITADEL_DATABASE_POSTGRES_DSN', value: databaseUrl },
|
|
1552
1620
|
{ key: 'ZITADEL_FIRSTINSTANCE_SKIP', value: 'true' },
|
|
1553
1621
|
{ key: 'ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED', value: 'true' },
|
|
1554
1622
|
// A username is unique per organisation, and the login name is `name@<org domain>`. Without it a
|
|
@@ -1638,13 +1706,24 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1638
1706
|
await deps.coolify.bulkUpsertEnvs(brokerAppId, brokerEnvs);
|
|
1639
1707
|
// Deploy an app that was just created, is not running (a throw after the
|
|
1640
1708
|
// create left it never deployed, or it stopped), or whose envs changed.
|
|
1641
|
-
|
|
1709
|
+
// A move that started ZITADEL on the TLS database and never saw it healthy there (#746): start it
|
|
1710
|
+
// again and wait, so sign-in is not left down by a plan that finds nothing to do.
|
|
1711
|
+
const zitadelUnconfirmed = Boolean(recorded[SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY]);
|
|
1712
|
+
if (zitadelApp.justCreated ||
|
|
1713
|
+
!zitadelApp.running ||
|
|
1714
|
+
zitadelEnvsChanged ||
|
|
1715
|
+
zitadelRunOptionsNew ||
|
|
1716
|
+
zitadelUnconfirmed) {
|
|
1642
1717
|
await deps.coolify.deploy(zitadelAppId);
|
|
1643
1718
|
await record(PLATFORM_AUTH_ZITADEL_DEPLOYED_SCOPE_KEY, zitadelAppId);
|
|
1644
1719
|
// Only now: the options are live once the deploy went out. A throw between
|
|
1645
1720
|
// the PATCH and here leaves the marker unset, so the next run deploys.
|
|
1646
1721
|
await record(PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, AUTH_ZITADEL_RUN_OPTIONS);
|
|
1647
1722
|
}
|
|
1723
|
+
if (zitadelUnconfirmed) {
|
|
1724
|
+
await waitHealthy(withCallTimeout(deps.coolify), zitadelAppId, runtime, label, 'ZITADEL');
|
|
1725
|
+
await record(SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, '');
|
|
1726
|
+
}
|
|
1648
1727
|
if (loginApp.justCreated || !loginApp.running || loginEnvsChanged) {
|
|
1649
1728
|
await deps.coolify.deploy(loginAppId);
|
|
1650
1729
|
}
|
|
@@ -2361,7 +2440,7 @@ const STALWART_READY_PATH = '/healthz/ready';
|
|
|
2361
2440
|
/** Where the server the app was created on is recorded, so the MX record follows the app, not a later pick. */
|
|
2362
2441
|
export const PLATFORM_MAIL_SERVER_SCOPE_KEY = 'coolify_platform_mail_server';
|
|
2363
2442
|
const PLATFORM_MAIL_PROJECT_SCOPE_KEY = 'coolify_project_platform_mail';
|
|
2364
|
-
const PLATFORM_MAIL_APP_SCOPE_KEY =
|
|
2443
|
+
const PLATFORM_MAIL_APP_SCOPE_KEY = MAIL_APP_SCOPE_KEY;
|
|
2365
2444
|
const PLATFORM_MAIL_RELAY_APP_SCOPE_KEY = 'coolify_platform_mail_relay_app';
|
|
2366
2445
|
const PLATFORM_MAIL_TUNNEL_SCOPE_KEY = 'cloudflare_platform_mail_tunnel';
|
|
2367
2446
|
/**
|
|
@@ -2410,7 +2489,7 @@ export function defaultStalwartForInstance(input) {
|
|
|
2410
2489
|
user: input.user,
|
|
2411
2490
|
});
|
|
2412
2491
|
return {
|
|
2413
|
-
async applySettings({ mailHost, apex, oidc }) {
|
|
2492
|
+
async applySettings({ mailHost, apex, oidc, dataStore }) {
|
|
2414
2493
|
// A fresh Stalwart (0.16) is in bootstrap mode and refuses every write
|
|
2415
2494
|
// but the `Bootstrap` object, and loads that object only on its next
|
|
2416
2495
|
// start, so write it and hand the restart to the caller. A server
|
|
@@ -2418,9 +2497,29 @@ export function defaultStalwartForInstance(input) {
|
|
|
2418
2497
|
// carries the generated `admin@<apex>` secret: this step signs in with
|
|
2419
2498
|
// the recovery admin and never needs it.
|
|
2420
2499
|
if ((await instance.client.probe()) === 'bootstrap') {
|
|
2500
|
+
if (!dataStore.host ||
|
|
2501
|
+
!dataStore.database ||
|
|
2502
|
+
!dataStore.user ||
|
|
2503
|
+
!Number.isInteger(dataStore.port) ||
|
|
2504
|
+
dataStore.port < 1 ||
|
|
2505
|
+
dataStore.port > 65535) {
|
|
2506
|
+
throw new Error('platform:mail: the mail data store is not a usable Postgres address');
|
|
2507
|
+
}
|
|
2421
2508
|
await instance.client.update('Bootstrap', 'singleton', {
|
|
2422
2509
|
serverHostname: mailHost,
|
|
2423
2510
|
defaultDomain: apex,
|
|
2511
|
+
// The password is read from the app's env, never written into Stalwart's settings.
|
|
2512
|
+
dataStore: {
|
|
2513
|
+
'@type': 'PostgreSql',
|
|
2514
|
+
host: dataStore.host,
|
|
2515
|
+
port: dataStore.port,
|
|
2516
|
+
database: dataStore.database,
|
|
2517
|
+
authUsername: dataStore.user,
|
|
2518
|
+
authSecret: {
|
|
2519
|
+
'@type': 'EnvironmentVariable',
|
|
2520
|
+
variableName: MAIL_DATABASE_PASSWORD_ENV,
|
|
2521
|
+
},
|
|
2522
|
+
},
|
|
2424
2523
|
// The server sits behind a tunnel; no ACME challenge can reach it.
|
|
2425
2524
|
requestTlsCertificate: false,
|
|
2426
2525
|
});
|
|
@@ -2535,7 +2634,7 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2535
2634
|
const zone = input.zone ?? apex;
|
|
2536
2635
|
const label = target.kind === 'org' ? `own step coolify:own-mail for ${target.id}` : 'platform:mail';
|
|
2537
2636
|
const keyDescription = target.kind === 'org' ? `foundry:org:${target.id}:admin` : PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION;
|
|
2538
|
-
|
|
2637
|
+
let recorded = scope.recordedIds();
|
|
2539
2638
|
const runtime = deps.runtime ?? {};
|
|
2540
2639
|
const sleep = runtime.sleep ?? defaultSleep;
|
|
2541
2640
|
const record = async (key, id) => {
|
|
@@ -2552,6 +2651,15 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2552
2651
|
const imageTagBlocked = mailImageTagBlocked(imageTag);
|
|
2553
2652
|
if (imageTagBlocked)
|
|
2554
2653
|
throw new Error(imageTagBlocked);
|
|
2654
|
+
// A server whose data is still on its volume is never provisioned over: the plan's confirmed switch
|
|
2655
|
+
// deletes it first, and without that this step stops before any write.
|
|
2656
|
+
if (mailStoreIsLegacy(recorded)) {
|
|
2657
|
+
if (!input.switchStore) {
|
|
2658
|
+
throw new Error(`${label}: the mail server still stores its data on a volume; confirm the mail-store switch in the plan before applying`);
|
|
2659
|
+
}
|
|
2660
|
+
await switchMailStoreToPostgres(scope, deps.coolify, target, environment.id, label);
|
|
2661
|
+
recorded = scope.recordedIds();
|
|
2662
|
+
}
|
|
2555
2663
|
// One server for both the app and the MX record: recorded the first time,
|
|
2556
2664
|
// read back after, so a later run never points `mx.<apex>` somewhere the
|
|
2557
2665
|
// app is not.
|
|
@@ -2562,6 +2670,32 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2562
2670
|
const serverIpBlocked = mxRecord ? mailServerIpBlocked(server.ip, mxRecord.serverIpv4) : null;
|
|
2563
2671
|
if (serverIpBlocked)
|
|
2564
2672
|
throw new Error(serverIpBlocked);
|
|
2673
|
+
// ---- the data store: a Postgres, created and recorded before the app ----------
|
|
2674
|
+
// A crash after the create resumes with the recorded id and never makes a second database. A crash
|
|
2675
|
+
// before the app exists leaves only this empty database behind; the next run uses it.
|
|
2676
|
+
let mailDbId = recorded[MAIL_DATABASE_SCOPE_KEY];
|
|
2677
|
+
if (!mailDbId) {
|
|
2678
|
+
const found = (await deps.coolify.listDatabases()).find((d) => d.name === target.names.mailDb && d.environment_id === environment.id);
|
|
2679
|
+
if (found) {
|
|
2680
|
+
mailDbId = found.uuid;
|
|
2681
|
+
}
|
|
2682
|
+
else {
|
|
2683
|
+
const created = await deps.coolify.createPostgresDatabase({
|
|
2684
|
+
project_uuid: projectUuid,
|
|
2685
|
+
server_uuid: server.uuid,
|
|
2686
|
+
environment_name: environment.name,
|
|
2687
|
+
environment_uuid: environment.uuid,
|
|
2688
|
+
name: target.names.mailDb,
|
|
2689
|
+
postgres_db: MAIL_DATABASE_NAME,
|
|
2690
|
+
postgres_user: MAIL_DATABASE_NAME,
|
|
2691
|
+
});
|
|
2692
|
+
mailDbId = created.uuid;
|
|
2693
|
+
}
|
|
2694
|
+
await record(MAIL_DATABASE_SCOPE_KEY, mailDbId);
|
|
2695
|
+
}
|
|
2696
|
+
const mailDb = await getDatabaseStarted(deps.coolify, mailDbId);
|
|
2697
|
+
const dataStore = mailDataStore(mailDb.internal_db_url, label);
|
|
2698
|
+
runtime.onSecret?.(dataStore.password);
|
|
2565
2699
|
const app = await findOrCreatePlatformApp(deps.coolify, target.names.mailApp, environment, async () => {
|
|
2566
2700
|
return deps.coolify.createDockerImageApplication({
|
|
2567
2701
|
project_uuid: projectUuid,
|
|
@@ -2621,6 +2755,7 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2621
2755
|
const recoveryGenerated = await ensureGeneratedEnv(deps.coolify, appId, 'STALWART_RECOVERY_ADMIN', () => `${STALWART_RECOVERY_USER}:${randomBytes(18).toString('base64')}`);
|
|
2622
2756
|
const envs = [
|
|
2623
2757
|
{ key: 'STALWART_PUBLIC_URL', value: `https://${hostname}` },
|
|
2758
|
+
{ key: MAIL_DATABASE_PASSWORD_ENV, value: dataStore.password },
|
|
2624
2759
|
];
|
|
2625
2760
|
if (recoveryGenerated)
|
|
2626
2761
|
envs.push({ key: 'STALWART_RECOVERY_ADMIN', value: recoveryGenerated });
|
|
@@ -2810,13 +2945,15 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2810
2945
|
mailHost: hostname,
|
|
2811
2946
|
apex,
|
|
2812
2947
|
oidc: { issuerUrl: issuer, audience: project.id },
|
|
2948
|
+
dataStore: dataStore.address,
|
|
2813
2949
|
},
|
|
2814
2950
|
});
|
|
2951
|
+
await record(MAIL_STORE_SCOPE_KEY, MAIL_STORE_POSTGRES);
|
|
2815
2952
|
await record(PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, project.id);
|
|
2816
2953
|
await record(PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED);
|
|
2817
2954
|
}
|
|
2818
2955
|
catch (error) {
|
|
2819
|
-
throw redactedMailError(error, [password]);
|
|
2956
|
+
throw redactedMailError(error, [password, dataStore.password]);
|
|
2820
2957
|
}
|
|
2821
2958
|
}
|
|
2822
2959
|
else if (!recordedProjectId) {
|
|
@@ -2851,8 +2988,10 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2851
2988
|
mailHost: hostname,
|
|
2852
2989
|
apex,
|
|
2853
2990
|
oidc: { issuerUrl: issuer, audience: project.id },
|
|
2991
|
+
dataStore: dataStore.address,
|
|
2854
2992
|
},
|
|
2855
2993
|
});
|
|
2994
|
+
await record(MAIL_STORE_SCOPE_KEY, MAIL_STORE_POSTGRES);
|
|
2856
2995
|
await record(PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED);
|
|
2857
2996
|
// Recorded and persisted before the secret leaves this function: if
|
|
2858
2997
|
// delivery fails for good, the next plan reports the lost key rather
|
|
@@ -2875,7 +3014,7 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2875
3014
|
await record(PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, appId);
|
|
2876
3015
|
}
|
|
2877
3016
|
catch (error) {
|
|
2878
|
-
throw redactedMailError(error, [password]);
|
|
3017
|
+
throw redactedMailError(error, [password, dataStore.password]);
|
|
2879
3018
|
}
|
|
2880
3019
|
}
|
|
2881
3020
|
// Last, after every write the applied values depend on, so a run that
|
|
@@ -2884,6 +3023,79 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2884
3023
|
await record(PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, imageTag);
|
|
2885
3024
|
return { resourceId: appId };
|
|
2886
3025
|
}
|
|
3026
|
+
/**
|
|
3027
|
+
* Where Stalwart reaches its database, and the password it reads from its env. Parsed from Coolify's
|
|
3028
|
+
* `internal_db_url` inside a try: Node's parse error carries the whole input, credential included, so it
|
|
3029
|
+
* is replaced with a fixed message.
|
|
3030
|
+
*/
|
|
3031
|
+
function mailDataStore(internalDbUrl, label) {
|
|
3032
|
+
if (!internalDbUrl) {
|
|
3033
|
+
throw new Error(`${label}: the mail database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
3034
|
+
}
|
|
3035
|
+
try {
|
|
3036
|
+
const url = new URL(internalDbUrl);
|
|
3037
|
+
const address = {
|
|
3038
|
+
host: url.hostname,
|
|
3039
|
+
port: url.port ? Number(url.port) : 5432,
|
|
3040
|
+
database: decodeURIComponent(url.pathname.replace(/^\//, '')),
|
|
3041
|
+
user: decodeURIComponent(url.username),
|
|
3042
|
+
};
|
|
3043
|
+
const password = decodeURIComponent(url.password);
|
|
3044
|
+
if (!address.host ||
|
|
3045
|
+
!address.database ||
|
|
3046
|
+
!address.user ||
|
|
3047
|
+
!password ||
|
|
3048
|
+
!Number.isInteger(address.port)) {
|
|
3049
|
+
throw new Error('incomplete');
|
|
3050
|
+
}
|
|
3051
|
+
return { address, password };
|
|
3052
|
+
}
|
|
3053
|
+
catch {
|
|
3054
|
+
throw new Error(`${label}: the mail database's internal_db_url could not be parsed`);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* The recorded ids a recreated Stalwart makes stale: the app and what was deployed or applied to it, and the
|
|
3059
|
+
* admin key minted in it (the new server has none, so it is minted and delivered again). Everything else
|
|
3060
|
+
* stays: the project, the server, the tunnel and its connector, the DNS names, the internal address and the
|
|
3061
|
+
* published ports all describe things the new app reuses.
|
|
3062
|
+
*/
|
|
3063
|
+
// A function, not a constant: `platform-diff.ts` and this file import each other, so its keys are not
|
|
3064
|
+
// all initialised yet when this module is evaluated.
|
|
3065
|
+
const mailStoreSwitchClearedKeys = () => [
|
|
3066
|
+
PLATFORM_MAIL_APP_SCOPE_KEY,
|
|
3067
|
+
PLATFORM_MAIL_DEPLOYED_SCOPE_KEY,
|
|
3068
|
+
PLATFORM_MAIL_DEPLOYED_HOSTNAME_SCOPE_KEY,
|
|
3069
|
+
PLATFORM_MAIL_DEPLOYED_GIT_REF_SCOPE_KEY,
|
|
3070
|
+
PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY,
|
|
3071
|
+
PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY,
|
|
3072
|
+
PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY,
|
|
3073
|
+
PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY,
|
|
3074
|
+
PLATFORM_MAIL_ROUTE_SCOPE_KEY,
|
|
3075
|
+
];
|
|
3076
|
+
/**
|
|
3077
|
+
* Deletes the volume-backed Stalwart app (Coolify's delete removes its volumes) and clears the ids above.
|
|
3078
|
+
* The delete comes first: an app left behind after the ids are cleared would be found by name and reused,
|
|
3079
|
+
* still on its volume. A run that dies between the two finds no app by that name on the next run and only
|
|
3080
|
+
* clears. An app of that name whose uuid is not the recorded one is not deleted.
|
|
3081
|
+
*/
|
|
3082
|
+
async function switchMailStoreToPostgres(scope, coolify, target, environmentId, label) {
|
|
3083
|
+
const recorded = scope.recordedIds();
|
|
3084
|
+
const live = await coolify.findApplicationByName(target.names.mailApp, {
|
|
3085
|
+
environmentIds: [environmentId],
|
|
3086
|
+
});
|
|
3087
|
+
if (live) {
|
|
3088
|
+
if (live.uuid !== recorded[PLATFORM_MAIL_APP_SCOPE_KEY]) {
|
|
3089
|
+
throw new Error(`${label}: the app named ${target.names.mailApp} is not the recorded mail server; not deleting it`);
|
|
3090
|
+
}
|
|
3091
|
+
await coolify.deleteApplication(live.uuid);
|
|
3092
|
+
}
|
|
3093
|
+
for (const key of mailStoreSwitchClearedKeys()) {
|
|
3094
|
+
if (recorded[key])
|
|
3095
|
+
scope.recordScopeId(key, '');
|
|
3096
|
+
}
|
|
3097
|
+
await scope.persist();
|
|
3098
|
+
}
|
|
2887
3099
|
/** The platform's mail project: the shared `platform` project, its id recorded by the mail step under its own key. */
|
|
2888
3100
|
const platformMailProject = {
|
|
2889
3101
|
async ensure(coolify) {
|
|
@@ -2892,7 +3104,7 @@ const platformMailProject = {
|
|
|
2892
3104
|
},
|
|
2893
3105
|
};
|
|
2894
3106
|
/** The platform's mail server: {@link provisionMailServer} with {@link platformTarget} and the platform's own inputs. */
|
|
2895
|
-
async function provisionPlatformMail(context, deps) {
|
|
3107
|
+
async function provisionPlatformMail(context, deps, switchStore = false) {
|
|
2896
3108
|
const { platform } = context;
|
|
2897
3109
|
const scope = scopeFromPlatform(context);
|
|
2898
3110
|
return provisionMailServer(scope, deps, platformTarget(), {
|
|
@@ -2900,6 +3112,7 @@ async function provisionPlatformMail(context, deps) {
|
|
|
2900
3112
|
apex: platform.apex,
|
|
2901
3113
|
mailHost: platformMailMxHost(platform.apex),
|
|
2902
3114
|
imageTag: platform.mail.gitRef,
|
|
3115
|
+
switchStore,
|
|
2903
3116
|
hostPorts: platformMailHostPorts(platform),
|
|
2904
3117
|
issuerUrl: `https://${platform.auth.hostname}`,
|
|
2905
3118
|
mxRecord: { serverIpv4: platform.serverIpv4 },
|
|
@@ -6068,7 +6281,7 @@ async function waitForInstance(url, runtime, sleep, label = 'platform:auth') {
|
|
|
6068
6281
|
await sleep(interval);
|
|
6069
6282
|
}
|
|
6070
6283
|
}
|
|
6071
|
-
async function resolvePlatformServer(coolify) {
|
|
6284
|
+
export async function resolvePlatformServer(coolify) {
|
|
6072
6285
|
const servers = await coolify.listServers();
|
|
6073
6286
|
const server = servers.find((s) => s.is_usable && s.is_reachable) ?? servers[0];
|
|
6074
6287
|
if (!server)
|
|
@@ -6081,7 +6294,7 @@ async function resolvePlatformServer(coolify) {
|
|
|
6081
6294
|
* file holds no instance of that class to call it on. `running` says whether
|
|
6082
6295
|
* the app was already running or starting; the caller deploys it when not.
|
|
6083
6296
|
*/
|
|
6084
|
-
async function findOrCreatePlatformApp(coolify, name, environment, create) {
|
|
6297
|
+
export async function findOrCreatePlatformApp(coolify, name, environment, create) {
|
|
6085
6298
|
const existing = await coolify.findApplicationByName(name, {
|
|
6086
6299
|
environmentIds: [environment.id],
|
|
6087
6300
|
});
|
|
@@ -6236,7 +6449,7 @@ export function brokerDeployRecordValue(brokerAppId, authAppsRaw) {
|
|
|
6236
6449
|
return JSON.stringify(record);
|
|
6237
6450
|
}
|
|
6238
6451
|
/** Whether any desired env is missing from the app, or set to another value. */
|
|
6239
|
-
async function envsDiffer(coolify, appId, desired) {
|
|
6452
|
+
export async function envsDiffer(coolify, appId, desired) {
|
|
6240
6453
|
const live = new Map((await coolify.listEnvs(appId)).filter((e) => !e.is_preview).map((e) => [e.key, e.value]));
|
|
6241
6454
|
return desired.some((e) => live.get(e.key) !== e.value);
|
|
6242
6455
|
}
|
|
@@ -8177,11 +8390,12 @@ const PLATFORM_BACKUP_DEFAULT_FREQUENCY = 'daily';
|
|
|
8177
8390
|
* The database a service's schedule must name. A schedule that names none
|
|
8178
8391
|
* dumps the Coolify resource's first database; Archon's is named here anyway,
|
|
8179
8392
|
* so a resource whose first database is not {@link ARCHON_POSTGRES_DB} is still
|
|
8180
|
-
* dumped right. Every other service runs on its
|
|
8181
|
-
* needs no entry.
|
|
8393
|
+
* dumped right. Mail names its database the same way. Every other service runs on its
|
|
8394
|
+
* resource's first database and needs no entry.
|
|
8182
8395
|
*/
|
|
8183
8396
|
const PLATFORM_BACKUP_DATABASE_NAMES = {
|
|
8184
8397
|
archon: ARCHON_POSTGRES_DB,
|
|
8398
|
+
mail: MAIL_DATABASE_NAME,
|
|
8185
8399
|
};
|
|
8186
8400
|
/**
|
|
8187
8401
|
* The one schedule both the reader and {@link provisionPlatformBackup} treat
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { CoolifyPort } from './executor.js';
|
|
2
|
+
import type { OwnFilesR2Keys } from './own-services.js';
|
|
3
|
+
/**
|
|
4
|
+
* The nightly backup of a company's sign-in database (#746). Coolify's database backups cover Coolify
|
|
5
|
+
* databases only, and the TLS database is an application, so the schedule is a Coolify scheduled task of
|
|
6
|
+
* that application (`POST /applications/{uuid}/scheduled-tasks`, read from Coolify 4.4.2's
|
|
7
|
+
* `ScheduledTasksController`). Coolify runs the task's command with `docker exec` in the database's
|
|
8
|
+
* container at the task's frequency. The command dumps with `pg_dump` and puts the file in the company's
|
|
9
|
+
* own backup bucket, with a storage key that reaches that bucket alone (minted through the same host
|
|
10
|
+
* ports as the company's files bucket key, `ensureSigninDbBackupTarget`). The platform's backup key never
|
|
11
|
+
* reaches a company's container. It keeps nothing: Coolify's default of keeping every dump applies.
|
|
12
|
+
*/
|
|
13
|
+
/** Recorded id of the scheduled task (set only after Coolify lists it back as enabled). */
|
|
14
|
+
export declare const SIGNIN_DB_BACKUP_TASK_SCOPE_KEY = "signin_db_backup_task";
|
|
15
|
+
export declare const SIGNIN_DB_BACKUP_TASK_NAME = "signin-db-backup";
|
|
16
|
+
/** Coolify's own name for "once a day", the frequency the platform's database backups use. */
|
|
17
|
+
export declare const SIGNIN_DB_BACKUP_FREQUENCY = "daily";
|
|
18
|
+
/** How long Coolify lets one run take, in seconds. */
|
|
19
|
+
export declare const SIGNIN_DB_BACKUP_TIMEOUT_SECONDS = 3600;
|
|
20
|
+
/** The company's own backup bucket and the storage key scoped to it. The key is a secret. */
|
|
21
|
+
export interface SigninDbBackupTarget {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
bucket: string;
|
|
24
|
+
/** R2 takes `auto`. */
|
|
25
|
+
region?: string;
|
|
26
|
+
accessKeyId: string;
|
|
27
|
+
secretAccessKey: string;
|
|
28
|
+
}
|
|
29
|
+
/** The host's bucket and storage key ports, the same ones the company's files bucket uses. */
|
|
30
|
+
export interface SigninDbBackupPort {
|
|
31
|
+
r2Buckets: {
|
|
32
|
+
ensureBucket(name: string): Promise<{
|
|
33
|
+
bucket: string;
|
|
34
|
+
endpoint: string;
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
r2Keys: OwnFilesR2Keys;
|
|
38
|
+
}
|
|
39
|
+
/** The token id of the company's sign-in backup key, recorded once it is in the database app's env. */
|
|
40
|
+
export declare const SIGNIN_DB_BACKUP_KEY_SCOPE_KEY = "signin_db_backup_key";
|
|
41
|
+
/** The token id minted and not yet in the env: deleted before any new mint. */
|
|
42
|
+
export declare const SIGNIN_DB_BACKUP_KEY_UNDELIVERED_SCOPE_KEY = "signin_db_backup_key_undelivered";
|
|
43
|
+
/** How long one call to the bucket or key port may take. */
|
|
44
|
+
export declare const SIGNIN_DB_BACKUP_PORT_TIMEOUT_MS = 30000;
|
|
45
|
+
/**
|
|
46
|
+
* The company's own bucket and a storage key that names that bucket alone. A key already in the
|
|
47
|
+
* database app's env whose token id is recorded is kept. Otherwise any earlier token of this company
|
|
48
|
+
* (recorded, or minted and never confirmed) is deleted first, so there are never two live tokens, then
|
|
49
|
+
* one is minted, registered as a secret and recorded as undelivered; the caller writes it to the env and
|
|
50
|
+
* then records it as delivered. A failed bucket or mint throws before the database is deployed or the
|
|
51
|
+
* move stops anything. Errors name the label only.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ensureSigninDbBackupTarget(port: SigninDbBackupPort, input: {
|
|
54
|
+
bucketName: string;
|
|
55
|
+
label: string;
|
|
56
|
+
recorded: Readonly<Record<string, string>>;
|
|
57
|
+
/** What the database app's env holds now. */
|
|
58
|
+
held: {
|
|
59
|
+
keyId?: string;
|
|
60
|
+
secret?: string;
|
|
61
|
+
};
|
|
62
|
+
record(key: string, id: string): Promise<void>;
|
|
63
|
+
onSecret?(value: string): void;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
target: SigninDbBackupTarget;
|
|
66
|
+
tokenId: string;
|
|
67
|
+
fresh: boolean;
|
|
68
|
+
}>;
|
|
69
|
+
/** The env of the database app that the backup command reads. The command itself holds no value. */
|
|
70
|
+
export declare const SIGNIN_DB_BACKUP_ENV: {
|
|
71
|
+
readonly endpoint: "BACKUP_S3_ENDPOINT";
|
|
72
|
+
readonly bucket: "BACKUP_S3_BUCKET";
|
|
73
|
+
readonly region: "BACKUP_S3_REGION";
|
|
74
|
+
readonly keyId: "BACKUP_S3_KEY_ID";
|
|
75
|
+
readonly secret: "BACKUP_S3_SECRET";
|
|
76
|
+
readonly prefix: "BACKUP_S3_PREFIX";
|
|
77
|
+
};
|
|
78
|
+
/** Where in the bucket a company's dumps go: `signin-db/<slug>/signin-db-<UTC time>.dump`. */
|
|
79
|
+
export declare function signinDbBackupPrefix(slug: string): string;
|
|
80
|
+
/** The env entries the backup command needs, for the database app. */
|
|
81
|
+
export declare function signinDbBackupEnvs(target: SigninDbBackupTarget, slug: string): Array<{
|
|
82
|
+
key: string;
|
|
83
|
+
value: string;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* The command Coolify runs in the container. A custom-format dump goes to a temporary file (an upload
|
|
87
|
+
* needs a length), is signed with SigV4 and put with curl, and the file is removed on every exit. The
|
|
88
|
+
* credentials reach curl on its standard input as a config, never in an argument. Every failure ends the
|
|
89
|
+
* run non-zero, and curl's error text names the HTTP status only. No single quote: Coolify wraps the
|
|
90
|
+
* command in single quotes.
|
|
91
|
+
*/
|
|
92
|
+
export declare function signinDbBackupCommand(user: string, database: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* Makes sure the database app has its backup task, and returns its uuid. The task is found by name
|
|
95
|
+
* before it is created, and updated to the full wanted body every time (Coolify hides the stored
|
|
96
|
+
* command from a token without the sensitive-read ability, so it cannot be compared). The task is then
|
|
97
|
+
* read back and must be listed enabled with the wanted frequency and timeout, or this throws: a
|
|
98
|
+
* schedule that cannot be seen is not a backup. The caller records the uuid. Errors name the label only.
|
|
99
|
+
*/
|
|
100
|
+
export declare function ensureSigninDbBackupTask(coolify: Pick<CoolifyPort, 'raw'>, appId: string, label: string, db: {
|
|
101
|
+
user: string;
|
|
102
|
+
database: string;
|
|
103
|
+
}): Promise<string>;
|
|
104
|
+
/** A company on the TLS database whose nightly dump is not scheduled yet: the sign-in step plans again to schedule it. */
|
|
105
|
+
export declare function signinDbBackupMissing(recordedIds: Readonly<Record<string, string>>): boolean;
|