@wtfalch/foundry 0.36.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/plan/backup-gate.d.ts +3 -3
- package/dist/plan/backup-gate.js +9 -4
- package/dist/plan/company-backups.d.ts +7 -2
- package/dist/plan/company-backups.js +13 -9
- package/dist/plan/diff.js +4 -0
- package/dist/plan/executor.d.ts +8 -0
- package/dist/plan/executor.js +13 -2
- 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.js +15 -4
- package/dist/plan/own-services.d.ts +6 -1
- package/dist/plan/own-services.js +5 -1
- package/dist/plan/plan-apply.js +7 -3
- 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 +55 -1
- package/dist/plan/platform-providers.js +205 -14
- package/dist/plan/uptime-worker.d.ts +20 -1
- package/dist/plan/uptime-worker.js +121 -3
- package/package.json +1 -1
|
@@ -17,10 +17,11 @@ 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';
|
|
25
26
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
26
27
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
@@ -53,6 +54,23 @@ export async function platformTunnels(deps, recorded, step) {
|
|
|
53
54
|
}
|
|
54
55
|
return deps.tunnels(edgeUrl);
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Request headers a client can set that ZITADEL reads to pick or name an
|
|
59
|
+
* instance (auth#241, auth#252). Cloudflare removes them from every inbound
|
|
60
|
+
* request on a sign-in hostname, because the tunnel routes ZITADEL's paths
|
|
61
|
+
* straight to ZITADEL and no app code sees them first.
|
|
62
|
+
*/
|
|
63
|
+
export const SIGNIN_STRIPPED_REQUEST_HEADERS = [
|
|
64
|
+
'X-Zitadel-Instance-Host',
|
|
65
|
+
'X-Zitadel-Public-Host',
|
|
66
|
+
'X-Zitadel-Forward-Host',
|
|
67
|
+
'X-Forwarded-Host',
|
|
68
|
+
];
|
|
69
|
+
/** The request header port for a platform step; undefined when edge is not recorded or the host supplied no factory. */
|
|
70
|
+
export async function platformRequestHeaders(deps, recorded) {
|
|
71
|
+
const edgeUrl = recorded[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
72
|
+
return edgeUrl && deps.requestHeaders ? deps.requestHeaders(edgeUrl) : undefined;
|
|
73
|
+
}
|
|
56
74
|
/** The Access port for a platform step; same address and same reason as {@link platformDns}. */
|
|
57
75
|
async function platformAccess(deps, recorded, step) {
|
|
58
76
|
const edgeUrl = recorded[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
@@ -355,7 +373,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
355
373
|
if (!apiKey)
|
|
356
374
|
blocked.heartbeatsApiKeyMissing = true;
|
|
357
375
|
}
|
|
358
|
-
await this.uptimeBlocked(platform, recordedIds, blocked);
|
|
376
|
+
await this.uptimeBlocked(platform, recordedIds, blocked, owner);
|
|
359
377
|
// #604: only once edge and sign-in are applied (the planner reports
|
|
360
378
|
// that case itself) and no provider is recorded yet.
|
|
361
379
|
if (platform.edge.enabled &&
|
|
@@ -423,7 +441,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
423
441
|
* checks the pin and Archon first, so none of the reads here happens before
|
|
424
442
|
* they hold.
|
|
425
443
|
*/
|
|
426
|
-
async uptimeBlocked(platform, recordedIds, blocked) {
|
|
444
|
+
async uptimeBlocked(platform, recordedIds, blocked, owner = {}) {
|
|
427
445
|
const deployed = Boolean(recordedIds[`cloudflare:worker:${PLATFORM_UPTIME_KEY}`]) &&
|
|
428
446
|
!recordedIds[PLATFORM_UPTIME_REMOVED_SCOPE_KEY];
|
|
429
447
|
if (!platform.uptime.enabled && !uptimeRecorded(recordedIds))
|
|
@@ -436,15 +454,41 @@ export class FoundryPlatformStepExecutor {
|
|
|
436
454
|
}
|
|
437
455
|
if (!platform.uptime.enabled)
|
|
438
456
|
return;
|
|
439
|
-
|
|
457
|
+
const heldValues = await heldUptimeValues(deps);
|
|
458
|
+
blocked.uptimeSecretsHeld = Object.keys(heldValues);
|
|
440
459
|
const accessTokenId = recordedIds[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY];
|
|
441
460
|
const accessHeld = Boolean((await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF)) &&
|
|
442
461
|
(await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF)));
|
|
443
462
|
blocked.uptimeHeld = {
|
|
444
463
|
token: Boolean(await deps.heldKeys.resolve(PLATFORM_UPTIME_TOKEN_REF)),
|
|
445
464
|
access: accessHeld,
|
|
446
|
-
vars: uptimeVarsDigest(deps.vars
|
|
465
|
+
vars: uptimeVarsDigest(uptimeEffectiveVars(deps.vars, deps.chat && owner.chat && recordedIds[PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY]
|
|
466
|
+
? {
|
|
467
|
+
homeserverUrl: `https://${owner.chat.agoraHost}`,
|
|
468
|
+
roomId: recordedIds[PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY],
|
|
469
|
+
}
|
|
470
|
+
: null)),
|
|
447
471
|
};
|
|
472
|
+
if (deps.chat && !heldValues.CHAT_ACCESS_TOKEN) {
|
|
473
|
+
if (!owner.chat) {
|
|
474
|
+
blocked.uptimeChat = {
|
|
475
|
+
pending: false,
|
|
476
|
+
blocked: "the uptime Worker's chat alerts are blocked, not planned: the owner company's chat is not applied yet",
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
else if (owner.chat.subjects.length === 0) {
|
|
480
|
+
blocked.uptimeChat = {
|
|
481
|
+
pending: false,
|
|
482
|
+
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",
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
blocked.uptimeChat = { pending: true };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else if (deps.chat) {
|
|
490
|
+
blocked.uptimeChat = { pending: false };
|
|
491
|
+
}
|
|
448
492
|
const edgeUrl = recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
449
493
|
// A token of the name with no id recorded cannot be minted over, and
|
|
450
494
|
// foundry does not remove what it did not make. With an id recorded the
|
|
@@ -650,7 +694,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
650
694
|
if (!this.mail) {
|
|
651
695
|
throw new Error('platform:mail needs FoundryPlatformStepExecutor to be constructed with its mail dependencies (#169)');
|
|
652
696
|
}
|
|
653
|
-
return provisionPlatformMail(context, this.limitedOnPlatform(this.mail, LIMITS.platformMail));
|
|
697
|
+
return provisionPlatformMail(context, this.limitedOnPlatform(this.mail, LIMITS.platformMail), action.desired.switchMailStore === true);
|
|
654
698
|
}
|
|
655
699
|
if (action.system === 'coolify' &&
|
|
656
700
|
action.kind === 'app' &&
|
|
@@ -1701,6 +1745,18 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1701
1745
|
if (!zoneId) {
|
|
1702
1746
|
throw new Error(`${label}: no Cloudflare zone for ${apex} to put ${hostname} in`);
|
|
1703
1747
|
}
|
|
1748
|
+
// Before the DNS record, so the hostname is never published without the rule.
|
|
1749
|
+
const requestHeaders = await network.requestHeaders();
|
|
1750
|
+
if (requestHeaders) {
|
|
1751
|
+
await requestHeaders.ensureRequestHeaderStrip(zoneId, {
|
|
1752
|
+
description: `strip spoofable sign-in headers: ${hostname}`,
|
|
1753
|
+
expression: `(http.host eq "${hostname}")`,
|
|
1754
|
+
headers: SIGNIN_STRIPPED_REQUEST_HEADERS,
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
console.error(`${label}: no request header port, so the sign-in hostname keeps X-Zitadel-* and X-Forwarded-Host from clients`);
|
|
1759
|
+
}
|
|
1704
1760
|
const dnsTarget = `${tunnel.id}.cfargotunnel.com`;
|
|
1705
1761
|
const dns = await network.dns();
|
|
1706
1762
|
const [dnsRecord] = await dns.findDnsRecord(apex, hostname, 'CNAME');
|
|
@@ -2332,7 +2388,7 @@ const STALWART_READY_PATH = '/healthz/ready';
|
|
|
2332
2388
|
/** Where the server the app was created on is recorded, so the MX record follows the app, not a later pick. */
|
|
2333
2389
|
export const PLATFORM_MAIL_SERVER_SCOPE_KEY = 'coolify_platform_mail_server';
|
|
2334
2390
|
const PLATFORM_MAIL_PROJECT_SCOPE_KEY = 'coolify_project_platform_mail';
|
|
2335
|
-
const PLATFORM_MAIL_APP_SCOPE_KEY =
|
|
2391
|
+
const PLATFORM_MAIL_APP_SCOPE_KEY = MAIL_APP_SCOPE_KEY;
|
|
2336
2392
|
const PLATFORM_MAIL_RELAY_APP_SCOPE_KEY = 'coolify_platform_mail_relay_app';
|
|
2337
2393
|
const PLATFORM_MAIL_TUNNEL_SCOPE_KEY = 'cloudflare_platform_mail_tunnel';
|
|
2338
2394
|
/**
|
|
@@ -2381,7 +2437,7 @@ export function defaultStalwartForInstance(input) {
|
|
|
2381
2437
|
user: input.user,
|
|
2382
2438
|
});
|
|
2383
2439
|
return {
|
|
2384
|
-
async applySettings({ mailHost, apex, oidc }) {
|
|
2440
|
+
async applySettings({ mailHost, apex, oidc, dataStore }) {
|
|
2385
2441
|
// A fresh Stalwart (0.16) is in bootstrap mode and refuses every write
|
|
2386
2442
|
// but the `Bootstrap` object, and loads that object only on its next
|
|
2387
2443
|
// start, so write it and hand the restart to the caller. A server
|
|
@@ -2389,9 +2445,29 @@ export function defaultStalwartForInstance(input) {
|
|
|
2389
2445
|
// carries the generated `admin@<apex>` secret: this step signs in with
|
|
2390
2446
|
// the recovery admin and never needs it.
|
|
2391
2447
|
if ((await instance.client.probe()) === 'bootstrap') {
|
|
2448
|
+
if (!dataStore.host ||
|
|
2449
|
+
!dataStore.database ||
|
|
2450
|
+
!dataStore.user ||
|
|
2451
|
+
!Number.isInteger(dataStore.port) ||
|
|
2452
|
+
dataStore.port < 1 ||
|
|
2453
|
+
dataStore.port > 65535) {
|
|
2454
|
+
throw new Error('platform:mail: the mail data store is not a usable Postgres address');
|
|
2455
|
+
}
|
|
2392
2456
|
await instance.client.update('Bootstrap', 'singleton', {
|
|
2393
2457
|
serverHostname: mailHost,
|
|
2394
2458
|
defaultDomain: apex,
|
|
2459
|
+
// The password is read from the app's env, never written into Stalwart's settings.
|
|
2460
|
+
dataStore: {
|
|
2461
|
+
'@type': 'PostgreSql',
|
|
2462
|
+
host: dataStore.host,
|
|
2463
|
+
port: dataStore.port,
|
|
2464
|
+
database: dataStore.database,
|
|
2465
|
+
authUsername: dataStore.user,
|
|
2466
|
+
authSecret: {
|
|
2467
|
+
'@type': 'EnvironmentVariable',
|
|
2468
|
+
variableName: MAIL_DATABASE_PASSWORD_ENV,
|
|
2469
|
+
},
|
|
2470
|
+
},
|
|
2395
2471
|
// The server sits behind a tunnel; no ACME challenge can reach it.
|
|
2396
2472
|
requestTlsCertificate: false,
|
|
2397
2473
|
});
|
|
@@ -2506,7 +2582,7 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2506
2582
|
const zone = input.zone ?? apex;
|
|
2507
2583
|
const label = target.kind === 'org' ? `own step coolify:own-mail for ${target.id}` : 'platform:mail';
|
|
2508
2584
|
const keyDescription = target.kind === 'org' ? `foundry:org:${target.id}:admin` : PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION;
|
|
2509
|
-
|
|
2585
|
+
let recorded = scope.recordedIds();
|
|
2510
2586
|
const runtime = deps.runtime ?? {};
|
|
2511
2587
|
const sleep = runtime.sleep ?? defaultSleep;
|
|
2512
2588
|
const record = async (key, id) => {
|
|
@@ -2523,6 +2599,15 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2523
2599
|
const imageTagBlocked = mailImageTagBlocked(imageTag);
|
|
2524
2600
|
if (imageTagBlocked)
|
|
2525
2601
|
throw new Error(imageTagBlocked);
|
|
2602
|
+
// A server whose data is still on its volume is never provisioned over: the plan's confirmed switch
|
|
2603
|
+
// deletes it first, and without that this step stops before any write.
|
|
2604
|
+
if (mailStoreIsLegacy(recorded)) {
|
|
2605
|
+
if (!input.switchStore) {
|
|
2606
|
+
throw new Error(`${label}: the mail server still stores its data on a volume; confirm the mail-store switch in the plan before applying`);
|
|
2607
|
+
}
|
|
2608
|
+
await switchMailStoreToPostgres(scope, deps.coolify, target, environment.id, label);
|
|
2609
|
+
recorded = scope.recordedIds();
|
|
2610
|
+
}
|
|
2526
2611
|
// One server for both the app and the MX record: recorded the first time,
|
|
2527
2612
|
// read back after, so a later run never points `mx.<apex>` somewhere the
|
|
2528
2613
|
// app is not.
|
|
@@ -2533,6 +2618,32 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2533
2618
|
const serverIpBlocked = mxRecord ? mailServerIpBlocked(server.ip, mxRecord.serverIpv4) : null;
|
|
2534
2619
|
if (serverIpBlocked)
|
|
2535
2620
|
throw new Error(serverIpBlocked);
|
|
2621
|
+
// ---- the data store: a Postgres, created and recorded before the app ----------
|
|
2622
|
+
// A crash after the create resumes with the recorded id and never makes a second database. A crash
|
|
2623
|
+
// before the app exists leaves only this empty database behind; the next run uses it.
|
|
2624
|
+
let mailDbId = recorded[MAIL_DATABASE_SCOPE_KEY];
|
|
2625
|
+
if (!mailDbId) {
|
|
2626
|
+
const found = (await deps.coolify.listDatabases()).find((d) => d.name === target.names.mailDb && d.environment_id === environment.id);
|
|
2627
|
+
if (found) {
|
|
2628
|
+
mailDbId = found.uuid;
|
|
2629
|
+
}
|
|
2630
|
+
else {
|
|
2631
|
+
const created = await deps.coolify.createPostgresDatabase({
|
|
2632
|
+
project_uuid: projectUuid,
|
|
2633
|
+
server_uuid: server.uuid,
|
|
2634
|
+
environment_name: environment.name,
|
|
2635
|
+
environment_uuid: environment.uuid,
|
|
2636
|
+
name: target.names.mailDb,
|
|
2637
|
+
postgres_db: MAIL_DATABASE_NAME,
|
|
2638
|
+
postgres_user: MAIL_DATABASE_NAME,
|
|
2639
|
+
});
|
|
2640
|
+
mailDbId = created.uuid;
|
|
2641
|
+
}
|
|
2642
|
+
await record(MAIL_DATABASE_SCOPE_KEY, mailDbId);
|
|
2643
|
+
}
|
|
2644
|
+
const mailDb = await getDatabaseStarted(deps.coolify, mailDbId);
|
|
2645
|
+
const dataStore = mailDataStore(mailDb.internal_db_url, label);
|
|
2646
|
+
runtime.onSecret?.(dataStore.password);
|
|
2536
2647
|
const app = await findOrCreatePlatformApp(deps.coolify, target.names.mailApp, environment, async () => {
|
|
2537
2648
|
return deps.coolify.createDockerImageApplication({
|
|
2538
2649
|
project_uuid: projectUuid,
|
|
@@ -2592,6 +2703,7 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2592
2703
|
const recoveryGenerated = await ensureGeneratedEnv(deps.coolify, appId, 'STALWART_RECOVERY_ADMIN', () => `${STALWART_RECOVERY_USER}:${randomBytes(18).toString('base64')}`);
|
|
2593
2704
|
const envs = [
|
|
2594
2705
|
{ key: 'STALWART_PUBLIC_URL', value: `https://${hostname}` },
|
|
2706
|
+
{ key: MAIL_DATABASE_PASSWORD_ENV, value: dataStore.password },
|
|
2595
2707
|
];
|
|
2596
2708
|
if (recoveryGenerated)
|
|
2597
2709
|
envs.push({ key: 'STALWART_RECOVERY_ADMIN', value: recoveryGenerated });
|
|
@@ -2781,13 +2893,15 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2781
2893
|
mailHost: hostname,
|
|
2782
2894
|
apex,
|
|
2783
2895
|
oidc: { issuerUrl: issuer, audience: project.id },
|
|
2896
|
+
dataStore: dataStore.address,
|
|
2784
2897
|
},
|
|
2785
2898
|
});
|
|
2899
|
+
await record(MAIL_STORE_SCOPE_KEY, MAIL_STORE_POSTGRES);
|
|
2786
2900
|
await record(PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, project.id);
|
|
2787
2901
|
await record(PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED);
|
|
2788
2902
|
}
|
|
2789
2903
|
catch (error) {
|
|
2790
|
-
throw redactedMailError(error, [password]);
|
|
2904
|
+
throw redactedMailError(error, [password, dataStore.password]);
|
|
2791
2905
|
}
|
|
2792
2906
|
}
|
|
2793
2907
|
else if (!recordedProjectId) {
|
|
@@ -2822,8 +2936,10 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2822
2936
|
mailHost: hostname,
|
|
2823
2937
|
apex,
|
|
2824
2938
|
oidc: { issuerUrl: issuer, audience: project.id },
|
|
2939
|
+
dataStore: dataStore.address,
|
|
2825
2940
|
},
|
|
2826
2941
|
});
|
|
2942
|
+
await record(MAIL_STORE_SCOPE_KEY, MAIL_STORE_POSTGRES);
|
|
2827
2943
|
await record(PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED);
|
|
2828
2944
|
// Recorded and persisted before the secret leaves this function: if
|
|
2829
2945
|
// delivery fails for good, the next plan reports the lost key rather
|
|
@@ -2846,7 +2962,7 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2846
2962
|
await record(PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, appId);
|
|
2847
2963
|
}
|
|
2848
2964
|
catch (error) {
|
|
2849
|
-
throw redactedMailError(error, [password]);
|
|
2965
|
+
throw redactedMailError(error, [password, dataStore.password]);
|
|
2850
2966
|
}
|
|
2851
2967
|
}
|
|
2852
2968
|
// Last, after every write the applied values depend on, so a run that
|
|
@@ -2855,6 +2971,79 @@ export async function provisionMailServer(scope, deps, target, input) {
|
|
|
2855
2971
|
await record(PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, imageTag);
|
|
2856
2972
|
return { resourceId: appId };
|
|
2857
2973
|
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Where Stalwart reaches its database, and the password it reads from its env. Parsed from Coolify's
|
|
2976
|
+
* `internal_db_url` inside a try: Node's parse error carries the whole input, credential included, so it
|
|
2977
|
+
* is replaced with a fixed message.
|
|
2978
|
+
*/
|
|
2979
|
+
function mailDataStore(internalDbUrl, label) {
|
|
2980
|
+
if (!internalDbUrl) {
|
|
2981
|
+
throw new Error(`${label}: the mail database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
2982
|
+
}
|
|
2983
|
+
try {
|
|
2984
|
+
const url = new URL(internalDbUrl);
|
|
2985
|
+
const address = {
|
|
2986
|
+
host: url.hostname,
|
|
2987
|
+
port: url.port ? Number(url.port) : 5432,
|
|
2988
|
+
database: decodeURIComponent(url.pathname.replace(/^\//, '')),
|
|
2989
|
+
user: decodeURIComponent(url.username),
|
|
2990
|
+
};
|
|
2991
|
+
const password = decodeURIComponent(url.password);
|
|
2992
|
+
if (!address.host ||
|
|
2993
|
+
!address.database ||
|
|
2994
|
+
!address.user ||
|
|
2995
|
+
!password ||
|
|
2996
|
+
!Number.isInteger(address.port)) {
|
|
2997
|
+
throw new Error('incomplete');
|
|
2998
|
+
}
|
|
2999
|
+
return { address, password };
|
|
3000
|
+
}
|
|
3001
|
+
catch {
|
|
3002
|
+
throw new Error(`${label}: the mail database's internal_db_url could not be parsed`);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* The recorded ids a recreated Stalwart makes stale: the app and what was deployed or applied to it, and the
|
|
3007
|
+
* admin key minted in it (the new server has none, so it is minted and delivered again). Everything else
|
|
3008
|
+
* stays: the project, the server, the tunnel and its connector, the DNS names, the internal address and the
|
|
3009
|
+
* published ports all describe things the new app reuses.
|
|
3010
|
+
*/
|
|
3011
|
+
// A function, not a constant: `platform-diff.ts` and this file import each other, so its keys are not
|
|
3012
|
+
// all initialised yet when this module is evaluated.
|
|
3013
|
+
const mailStoreSwitchClearedKeys = () => [
|
|
3014
|
+
PLATFORM_MAIL_APP_SCOPE_KEY,
|
|
3015
|
+
PLATFORM_MAIL_DEPLOYED_SCOPE_KEY,
|
|
3016
|
+
PLATFORM_MAIL_DEPLOYED_HOSTNAME_SCOPE_KEY,
|
|
3017
|
+
PLATFORM_MAIL_DEPLOYED_GIT_REF_SCOPE_KEY,
|
|
3018
|
+
PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY,
|
|
3019
|
+
PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY,
|
|
3020
|
+
PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY,
|
|
3021
|
+
PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY,
|
|
3022
|
+
PLATFORM_MAIL_ROUTE_SCOPE_KEY,
|
|
3023
|
+
];
|
|
3024
|
+
/**
|
|
3025
|
+
* Deletes the volume-backed Stalwart app (Coolify's delete removes its volumes) and clears the ids above.
|
|
3026
|
+
* The delete comes first: an app left behind after the ids are cleared would be found by name and reused,
|
|
3027
|
+
* still on its volume. A run that dies between the two finds no app by that name on the next run and only
|
|
3028
|
+
* clears. An app of that name whose uuid is not the recorded one is not deleted.
|
|
3029
|
+
*/
|
|
3030
|
+
async function switchMailStoreToPostgres(scope, coolify, target, environmentId, label) {
|
|
3031
|
+
const recorded = scope.recordedIds();
|
|
3032
|
+
const live = await coolify.findApplicationByName(target.names.mailApp, {
|
|
3033
|
+
environmentIds: [environmentId],
|
|
3034
|
+
});
|
|
3035
|
+
if (live) {
|
|
3036
|
+
if (live.uuid !== recorded[PLATFORM_MAIL_APP_SCOPE_KEY]) {
|
|
3037
|
+
throw new Error(`${label}: the app named ${target.names.mailApp} is not the recorded mail server; not deleting it`);
|
|
3038
|
+
}
|
|
3039
|
+
await coolify.deleteApplication(live.uuid);
|
|
3040
|
+
}
|
|
3041
|
+
for (const key of mailStoreSwitchClearedKeys()) {
|
|
3042
|
+
if (recorded[key])
|
|
3043
|
+
scope.recordScopeId(key, '');
|
|
3044
|
+
}
|
|
3045
|
+
await scope.persist();
|
|
3046
|
+
}
|
|
2858
3047
|
/** The platform's mail project: the shared `platform` project, its id recorded by the mail step under its own key. */
|
|
2859
3048
|
const platformMailProject = {
|
|
2860
3049
|
async ensure(coolify) {
|
|
@@ -2863,7 +3052,7 @@ const platformMailProject = {
|
|
|
2863
3052
|
},
|
|
2864
3053
|
};
|
|
2865
3054
|
/** The platform's mail server: {@link provisionMailServer} with {@link platformTarget} and the platform's own inputs. */
|
|
2866
|
-
async function provisionPlatformMail(context, deps) {
|
|
3055
|
+
async function provisionPlatformMail(context, deps, switchStore = false) {
|
|
2867
3056
|
const { platform } = context;
|
|
2868
3057
|
const scope = scopeFromPlatform(context);
|
|
2869
3058
|
return provisionMailServer(scope, deps, platformTarget(), {
|
|
@@ -2871,6 +3060,7 @@ async function provisionPlatformMail(context, deps) {
|
|
|
2871
3060
|
apex: platform.apex,
|
|
2872
3061
|
mailHost: platformMailMxHost(platform.apex),
|
|
2873
3062
|
imageTag: platform.mail.gitRef,
|
|
3063
|
+
switchStore,
|
|
2874
3064
|
hostPorts: platformMailHostPorts(platform),
|
|
2875
3065
|
issuerUrl: `https://${platform.auth.hostname}`,
|
|
2876
3066
|
mxRecord: { serverIpv4: platform.serverIpv4 },
|
|
@@ -8148,11 +8338,12 @@ const PLATFORM_BACKUP_DEFAULT_FREQUENCY = 'daily';
|
|
|
8148
8338
|
* The database a service's schedule must name. A schedule that names none
|
|
8149
8339
|
* dumps the Coolify resource's first database; Archon's is named here anyway,
|
|
8150
8340
|
* so a resource whose first database is not {@link ARCHON_POSTGRES_DB} is still
|
|
8151
|
-
* dumped right. Every other service runs on its
|
|
8152
|
-
* needs no entry.
|
|
8341
|
+
* dumped right. Mail names its database the same way. Every other service runs on its
|
|
8342
|
+
* resource's first database and needs no entry.
|
|
8153
8343
|
*/
|
|
8154
8344
|
const PLATFORM_BACKUP_DATABASE_NAMES = {
|
|
8155
8345
|
archon: ARCHON_POSTGRES_DB,
|
|
8346
|
+
mail: MAIL_DATABASE_NAME,
|
|
8156
8347
|
};
|
|
8157
8348
|
/**
|
|
8158
8349
|
* The one schedule both the reader and {@link provisionPlatformBackup} treat
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AccessPort } from './edge-access.js';
|
|
2
2
|
import type { PlatformWorkersPort } from './edge-workers.js';
|
|
3
|
+
import type { CoolifyPort } from './executor.js';
|
|
3
4
|
import type { PlatformStepExecutionContext } from './plan-apply.js';
|
|
4
5
|
/** The optional plain text variables the Worker reads, supplied by the host (not secrets). */
|
|
5
6
|
export declare const UPTIME_OPTIONAL_VARS: readonly ["CHAT_HOMESERVER_URL", "CHAT_ROOM_ID", "ALERT_EMAIL_FROM", "ALERT_EMAIL_TO"];
|
|
@@ -9,6 +10,19 @@ export type PlatformWorkersFactory = (edgeUrl: string) => PlatformWorkersPort |
|
|
|
9
10
|
export type PlatformUptimeAccessPort = Pick<AccessPort, 'createServiceToken' | 'removeServiceToken' | 'findServiceTokenByName'>;
|
|
10
11
|
/** Builds the Access port at edge's recorded address, the {@link PlatformWorkersFactory} way. */
|
|
11
12
|
export type PlatformUptimeAccessFactory = (edgeUrl: string) => PlatformUptimeAccessPort | Promise<PlatformUptimeAccessPort>;
|
|
13
|
+
/**
|
|
14
|
+
* What the uptime step needs to make its chat bot through chat-service. The bearer for
|
|
15
|
+
* `/_chat/internal/*` is `CHAT_INTERNAL_TOKEN` on Company Included's chat-service app, read here and
|
|
16
|
+
* never held, logged or put in an error.
|
|
17
|
+
*/
|
|
18
|
+
export interface PlatformUptimeChatDeps {
|
|
19
|
+
/** Reads that app's env (`OwnerChat.appId`). */
|
|
20
|
+
coolify: Pick<CoolifyPort, 'listEnvs'>;
|
|
21
|
+
/** The HTTP client; the global `fetch` by default. */
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
/** Milliseconds one call may take (default 10 seconds). */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* Dependencies of the uptime Worker step. `deliverSecret` and `heldKeys` are
|
|
14
28
|
* the host's: the minted token goes only to `deliverSecret`, and every secret
|
|
@@ -26,8 +40,13 @@ export interface PlatformUptimeDeps {
|
|
|
26
40
|
heldKeys: {
|
|
27
41
|
resolve(ref: string): Promise<string | null>;
|
|
28
42
|
};
|
|
29
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Host config for the Worker's optional plain text variables; an absent one is left unset. A set
|
|
45
|
+
* `CHAT_HOMESERVER_URL` or `CHAT_ROOM_ID` overrides the one foundry made.
|
|
46
|
+
*/
|
|
30
47
|
vars?: Partial<Record<(typeof UPTIME_OPTIONAL_VARS)[number], string>>;
|
|
48
|
+
/** Makes the Worker's chat bot and its token. Absent: the chat credential is held by hand. */
|
|
49
|
+
chat?: PlatformUptimeChatDeps;
|
|
31
50
|
}
|
|
32
51
|
/**
|
|
33
52
|
* What is held now: the optional secrets, and `UPTIME_HEARTBEAT_URL` (a ping
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
2
|
import { EdgeApiError } from '@wtfalch/edge';
|
|
3
3
|
import { uptimeArtifactUrl } from '../platform.js';
|
|
4
|
-
import { PLATFORM_EDGE_INTERNAL_URL_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_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_URL_REF, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_KV_TITLE, PLATFORM_UPTIME_OPTIONAL_SECRETS, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY, uptimeAccessTokenConflict, uptimeSecretNames, uptimeVarsDigest, } from './platform-diff.js';
|
|
4
|
+
import { PLATFORM_EDGE_INTERNAL_URL_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_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_CHAT_TOKEN_REF, PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_URL_REF, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_KV_TITLE, PLATFORM_UPTIME_OPTIONAL_SECRETS, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeSecretNames, uptimeVarsDigest, } from './platform-diff.js';
|
|
5
5
|
/** The Worker's cron trigger, from Archon's `workers/uptime/manifest.json`. */
|
|
6
6
|
const UPTIME_CRONS = ['* * * * *'];
|
|
7
7
|
/** The date Archon's own Worker config used. */
|
|
@@ -15,6 +15,84 @@ export const UPTIME_OPTIONAL_VARS = [
|
|
|
15
15
|
'ALERT_EMAIL_FROM',
|
|
16
16
|
'ALERT_EMAIL_TO',
|
|
17
17
|
];
|
|
18
|
+
/** How long one call to read chat-service's token or to ask it for the bot may take. */
|
|
19
|
+
const CHAT_CALL_TIMEOUT_MS = 10_000;
|
|
20
|
+
/** The bot's localpart in chat-service (`@uptime-bot:<server>`). */
|
|
21
|
+
const CHAT_BOT_LOCALPART = 'uptime-bot';
|
|
22
|
+
/** Runs `work` and gives up after `ms`; the error names `what` only. */
|
|
23
|
+
async function withinMs(ms, what, work) {
|
|
24
|
+
let timer;
|
|
25
|
+
try {
|
|
26
|
+
return await Promise.race([
|
|
27
|
+
work,
|
|
28
|
+
new Promise((_, reject) => {
|
|
29
|
+
timer = setTimeout(() => reject(new Error(`platform:uptime: ${what} did not answer within ${ms / 1000}s`)), ms);
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `PUT /_chat/internal/bots/uptime-bot` on the company's Agora host. Idempotent on the chat-service
|
|
39
|
+
* side: a repeat returns the room and no token unless `rotate` is set, so calling with `rotate` again
|
|
40
|
+
* revokes the previous token. Every message here is fixed text: the response body, the bearer and the
|
|
41
|
+
* token never reach an error.
|
|
42
|
+
*/
|
|
43
|
+
async function callChatBot(chat, owner, rotate) {
|
|
44
|
+
const timeoutMs = chat.timeoutMs ?? CHAT_CALL_TIMEOUT_MS;
|
|
45
|
+
let envs;
|
|
46
|
+
try {
|
|
47
|
+
envs = await withinMs(timeoutMs, 'reading the chat-service app', chat.coolify.listEnvs(owner.appId));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// The cause can carry a response body.
|
|
51
|
+
throw new Error('platform:uptime: the chat-service app could not be read');
|
|
52
|
+
}
|
|
53
|
+
const bearer = envs.find((e) => e.key === 'CHAT_INTERNAL_TOKEN' && !e.is_preview)?.value;
|
|
54
|
+
if (typeof bearer !== 'string' || !/^\S{32,}$/.test(bearer)) {
|
|
55
|
+
throw new Error('platform:uptime: chat-service holds no usable CHAT_INTERNAL_TOKEN');
|
|
56
|
+
}
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const response = await (chat.fetch ?? fetch)(`https://${owner.agoraHost}/_chat/internal/bots/${CHAT_BOT_LOCALPART}`, {
|
|
61
|
+
method: 'PUT',
|
|
62
|
+
headers: { authorization: `Bearer ${bearer}`, 'content-type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
displayName: 'Platform alerts',
|
|
65
|
+
room: { name: 'platform-alerts' },
|
|
66
|
+
members: [...owner.subjects],
|
|
67
|
+
...(rotate ? { rotate: true } : {}),
|
|
68
|
+
}),
|
|
69
|
+
redirect: 'error',
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
});
|
|
72
|
+
if (response.status !== 200) {
|
|
73
|
+
throw new Error(`platform:uptime: chat-service refused the bot request (HTTP ${response.status})`);
|
|
74
|
+
}
|
|
75
|
+
const body = (await response.json());
|
|
76
|
+
const { userId, roomId, accessToken } = body ?? {};
|
|
77
|
+
if (typeof userId !== 'string' ||
|
|
78
|
+
!userId ||
|
|
79
|
+
typeof roomId !== 'string' ||
|
|
80
|
+
!roomId ||
|
|
81
|
+
(accessToken !== undefined && (typeof accessToken !== 'string' || !accessToken))) {
|
|
82
|
+
throw new Error('platform:uptime: chat-service answered the bot request with an unusable body');
|
|
83
|
+
}
|
|
84
|
+
return { userId, roomId, ...(accessToken ? { accessToken } : {}) };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
// Our own messages pass; anything else (a network error, a bad JSON body) is replaced.
|
|
88
|
+
if (error instanceof Error && error.message.startsWith('platform:uptime: '))
|
|
89
|
+
throw error;
|
|
90
|
+
throw new Error('platform:uptime: chat-service did not answer the bot request');
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
18
96
|
/**
|
|
19
97
|
* What is held now: the optional secrets, and `UPTIME_HEARTBEAT_URL` (a ping
|
|
20
98
|
* URL that works as a capability, so a secret too), name to value. Callers
|
|
@@ -108,11 +186,51 @@ export async function provisionPlatformUptime(context, deps) {
|
|
|
108
186
|
}
|
|
109
187
|
const hostname = platform.archon.hostname;
|
|
110
188
|
const held = await heldUptimeValues(deps);
|
|
189
|
+
// The chat bot comes before the upload: its room is one of the Worker's variables. A call that fails
|
|
190
|
+
// stops the step here, and the next apply makes it again (the marker below says whether a token may
|
|
191
|
+
// already have been minted).
|
|
192
|
+
const ownerChat = context.owner?.chat ?? null;
|
|
193
|
+
let chatRoomId = recorded[PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY] || '';
|
|
194
|
+
if (deps.chat && ownerChat && ownerChat.subjects.length > 0) {
|
|
195
|
+
const lost = recorded[PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY];
|
|
196
|
+
if (held.CHAT_ACCESS_TOKEN) {
|
|
197
|
+
if (lost) {
|
|
198
|
+
context.recordScopeId(PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, '');
|
|
199
|
+
await context.persist?.();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Recorded and persisted before the route is called, because the call is what mints the token.
|
|
204
|
+
if (!lost) {
|
|
205
|
+
context.recordScopeId(PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, randomUUID());
|
|
206
|
+
await context.persist?.();
|
|
207
|
+
}
|
|
208
|
+
// A marker, or a room already made, means the bot exists and its token is not held: rotate.
|
|
209
|
+
const rotate = Boolean(lost) || Boolean(chatRoomId);
|
|
210
|
+
let bot = await callChatBot(deps.chat, ownerChat, rotate);
|
|
211
|
+
if (!bot.accessToken && !rotate)
|
|
212
|
+
bot = await callChatBot(deps.chat, ownerChat, true);
|
|
213
|
+
const chatToken = bot.accessToken;
|
|
214
|
+
if (!chatToken) {
|
|
215
|
+
throw new Error('platform:uptime: chat-service returned no token for the bot, even when asked to rotate it');
|
|
216
|
+
}
|
|
217
|
+
chatRoomId = bot.roomId;
|
|
218
|
+
context.recordScopeId(PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, chatRoomId);
|
|
219
|
+
await context.persist?.();
|
|
220
|
+
await attempt('holding the chat bot token', () => deps.deliverSecret(PLATFORM_UPTIME_CHAT_TOKEN_REF, chatToken));
|
|
221
|
+
context.recordScopeId(PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, '');
|
|
222
|
+
await context.persist?.();
|
|
223
|
+
held.CHAT_ACCESS_TOKEN = chatToken;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const workerVars = uptimeEffectiveVars(deps.vars, deps.chat && ownerChat && chatRoomId
|
|
227
|
+
? { homeserverUrl: `https://${ownerChat.agoraHost}`, roomId: chatRoomId }
|
|
228
|
+
: null);
|
|
111
229
|
const bindings = [
|
|
112
230
|
{ type: 'kv_namespace', name: 'UPTIME_KV', title: PLATFORM_UPTIME_KV_TITLE },
|
|
113
231
|
{ type: 'plain_text', name: 'ARCHON_BASE_URL', text: `https://${hostname}` },
|
|
114
232
|
...UPTIME_OPTIONAL_VARS.flatMap((name) => {
|
|
115
|
-
const text =
|
|
233
|
+
const text = workerVars[name];
|
|
116
234
|
return text ? [{ type: 'plain_text', name, text }] : [];
|
|
117
235
|
}),
|
|
118
236
|
];
|
|
@@ -223,7 +341,7 @@ export async function provisionPlatformUptime(context, deps) {
|
|
|
223
341
|
context.recordScopeId(PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, uptime.tag);
|
|
224
342
|
context.recordScopeId(PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, uptime.sha256);
|
|
225
343
|
context.recordScopeId(PLATFORM_UPTIME_APPLIED_HOSTNAME_SCOPE_KEY, hostname);
|
|
226
|
-
context.recordScopeId(PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, uptimeVarsDigest(
|
|
344
|
+
context.recordScopeId(PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, uptimeVarsDigest(workerVars));
|
|
227
345
|
context.recordScopeId(PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, uptimeSecretNames(Object.keys(held)));
|
|
228
346
|
return { resourceId: PLATFORM_UPTIME_SCRIPT_NAME };
|
|
229
347
|
}
|