@wtfalch/foundry 0.34.0 → 0.35.1
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/bootstrap.d.ts +1 -1
- package/dist/bootstrap/bootstrap.js +49 -11
- package/dist/bootstrap/declare.d.ts +12 -5
- package/dist/bootstrap/declare.js +33 -18
- package/dist/bootstrap/versions.d.ts +20 -0
- package/dist/bootstrap/versions.js +46 -0
- package/dist/calls-sfu.js +3 -3
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/migrations/0053_heartbeats_provider.sql +9 -0
- package/dist/migrations/0053_org_version_pin.sql +6 -0
- package/dist/org.d.ts +60 -14
- package/dist/org.js +118 -28
- package/dist/plan/backup-gate.d.ts +93 -0
- package/dist/plan/backup-gate.js +261 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +20 -3
- package/dist/plan/build-record.d.ts +56 -0
- package/dist/plan/build-record.js +109 -0
- package/dist/plan/calls-matrix.js +3 -3
- package/dist/plan/cloudflared-connector.js +2 -2
- package/dist/plan/company-backups.d.ts +31 -0
- package/dist/plan/company-backups.js +101 -0
- package/dist/plan/deploy-health.d.ts +12 -2
- package/dist/plan/deploy-health.js +3 -1
- package/dist/plan/destinations.d.ts +73 -0
- package/dist/plan/destinations.js +217 -0
- package/dist/plan/diff.d.ts +17 -8
- package/dist/plan/diff.js +131 -131
- package/dist/plan/edge-access.d.ts +15 -0
- package/dist/plan/edge-access.js +21 -2
- package/dist/plan/executor.d.ts +166 -7
- package/dist/plan/executor.js +547 -106
- package/dist/plan/health-targets.d.ts +1 -3
- package/dist/plan/health-targets.js +36 -36
- package/dist/plan/held-key-refs.d.ts +5 -0
- package/dist/plan/held-key-refs.js +5 -0
- package/dist/plan/hostnames.d.ts +10 -0
- package/dist/plan/hostnames.js +14 -0
- package/dist/plan/index.d.ts +5 -0
- package/dist/plan/index.js +3 -0
- package/dist/plan/live.d.ts +6 -0
- package/dist/plan/live.js +3 -2
- package/dist/plan/locked-hostnames.d.ts +2 -1
- package/dist/plan/locked-hostnames.js +16 -3
- package/dist/plan/login-host.d.ts +9 -20
- package/dist/plan/login-host.js +10 -25
- package/dist/plan/mail-relay.d.ts +48 -29
- package/dist/plan/mail-relay.js +145 -68
- package/dist/plan/network-move.d.ts +20 -0
- package/dist/plan/network-move.js +41 -0
- package/dist/plan/oikos-ingress.d.ts +111 -0
- package/dist/plan/oikos-ingress.js +231 -0
- package/dist/plan/org-names.d.ts +8 -2
- package/dist/plan/org-names.js +57 -1
- package/dist/plan/own-diff.d.ts +9 -8
- package/dist/plan/own-diff.js +42 -34
- package/dist/plan/own-hostnames.d.ts +27 -0
- package/dist/plan/own-hostnames.js +24 -0
- package/dist/plan/own-limits.d.ts +198 -7
- package/dist/plan/own-limits.js +163 -32
- package/dist/plan/own-services.js +3 -1
- package/dist/plan/own-sfu.js +3 -2
- package/dist/plan/plan-apply.d.ts +47 -7
- package/dist/plan/plan-apply.js +184 -38
- package/dist/plan/platform-diff.d.ts +133 -17
- package/dist/plan/platform-diff.js +231 -68
- package/dist/plan/platform-org.d.ts +79 -10
- package/dist/plan/platform-org.js +93 -13
- package/dist/plan/platform-providers.d.ts +118 -73
- package/dist/plan/platform-providers.js +770 -231
- package/dist/plan/proxy-labels.d.ts +57 -11
- package/dist/plan/proxy-labels.js +94 -14
- package/dist/plan/run-options.d.ts +19 -0
- package/dist/plan/run-options.js +46 -0
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +76 -8
- package/dist/platform.d.ts +32 -0
- package/dist/platform.js +16 -0
- package/dist/provision.d.ts +2 -0
- package/dist/provision.js +3 -1
- package/dist/slug.d.ts +3 -1
- package/dist/slug.js +25 -3
- package/dist/store/postgres.js +43 -1
- package/dist/store/schema.d.ts +36 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -3
- package/versions.digests.json +11 -0
package/dist/plan/plan-apply.js
CHANGED
|
@@ -7,20 +7,24 @@ import { AlreadyRunningError, StepFailedError } from '../runner/errors.js';
|
|
|
7
7
|
import { StaleLeaseError, } from '../store.js';
|
|
8
8
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
9
9
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
10
|
+
import { NetworkMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
11
|
+
import { readLiveBuilds } from './build-record.js';
|
|
10
12
|
import { unconfirmedUpgrades } from './confirm-upgrade.js';
|
|
11
|
-
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail,
|
|
13
|
+
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
12
14
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
13
15
|
import { BOULE_EDGE_ENTRY_KEY, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
14
16
|
import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
|
|
15
17
|
import { readLivePlatformResources, readLiveResources, } from './live.js';
|
|
16
18
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
17
19
|
import { agoraIssuerHostFor, loginHostServesAgora, readLoginHostDiscovery, } from './login-host.js';
|
|
18
|
-
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, } from './mail-relay.js';
|
|
20
|
+
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMTP_PORT_MAX, OWN_MAIL_SMTP_PORT_SCOPE_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, isOwnMailSmtpPort, mailRelayFingerprint, } from './mail-relay.js';
|
|
19
21
|
import { findNameClashes, nameClashMessage } from './org-names.js';
|
|
22
|
+
import { ownHostnames } from './own-hostnames.js';
|
|
20
23
|
import { OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, orgTarget } from './own-services.js';
|
|
21
|
-
import { PLATFORM_ARCHON_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
22
|
-
import { DEFAULT_ORG_PLATFORM_READINESS,
|
|
23
|
-
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
24
|
+
import { PLATFORM_ARCHON_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
25
|
+
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, dnsBlockedReason, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, ownerFilesFrom, ownerSignInFrom, platformOwnerUserIdFrom, serviceGitRefsOf, } from './platform-org.js';
|
|
26
|
+
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY, } from './platform-providers.js';
|
|
27
|
+
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, } from './proxy-labels.js';
|
|
24
28
|
import { liveWorkers, workerTraceScopeKeys } from './worker.js';
|
|
25
29
|
/** #441: the lease scope that serializes every mutation of one company, and the one for the shared platform. */
|
|
26
30
|
const companyScope = (orgId) => `company:${orgId}`;
|
|
@@ -160,6 +164,7 @@ export class FoundryPlanApply {
|
|
|
160
164
|
now;
|
|
161
165
|
leaseTtlMs;
|
|
162
166
|
appliedVersions;
|
|
167
|
+
backupGate;
|
|
163
168
|
platformDeps;
|
|
164
169
|
runner;
|
|
165
170
|
constructor(options) {
|
|
@@ -187,6 +192,7 @@ export class FoundryPlanApply {
|
|
|
187
192
|
this.now = options.now ?? (() => new Date());
|
|
188
193
|
this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
189
194
|
this.appliedVersions = options.appliedVersions;
|
|
195
|
+
this.backupGate = options.backupGate;
|
|
190
196
|
this.platformDeps = options.platform;
|
|
191
197
|
this.runner = options.runner;
|
|
192
198
|
}
|
|
@@ -360,6 +366,7 @@ export class FoundryPlanApply {
|
|
|
360
366
|
const reader = this.readers.coolify;
|
|
361
367
|
const live = project && reader.listProject ? await reader.listProject(project) : null;
|
|
362
368
|
const claimedElsewhere = [];
|
|
369
|
+
const platformRecorded = new Set(Object.values((await this.platformDeps?.store.getServiceIds()) ?? {}));
|
|
363
370
|
for (const [what, uuid] of [
|
|
364
371
|
['application', input.applicationUuid],
|
|
365
372
|
['database', input.databaseUuid],
|
|
@@ -367,6 +374,10 @@ export class FoundryPlanApply {
|
|
|
367
374
|
]) {
|
|
368
375
|
if (!uuid)
|
|
369
376
|
continue;
|
|
377
|
+
if (platformRecorded.has(uuid)) {
|
|
378
|
+
claimedElsewhere.push(`${what} ${uuid} is recorded by the platform`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
370
381
|
const owner = await this.store.findOtherOrgRecordingCoolifyId(uuid, id);
|
|
371
382
|
if (owner)
|
|
372
383
|
claimedElsewhere.push(`${what} ${uuid} is already recorded by org ${owner}`);
|
|
@@ -394,12 +405,50 @@ export class FoundryPlanApply {
|
|
|
394
405
|
await this.refuse(plan, `version change without confirmUpgrade naming: ${unconfirmed.map((u) => u.service).join(', ')}`);
|
|
395
406
|
throw new UpgradeConfirmationRequiredError(unconfirmed);
|
|
396
407
|
}
|
|
408
|
+
// F6: before step one. No override.
|
|
409
|
+
let backupId = null;
|
|
410
|
+
if (this.backupGate) {
|
|
411
|
+
try {
|
|
412
|
+
({ backupId } = await requireFreshBackupsForUpgrades(freshPlan, options.confirmUpgrade, {
|
|
413
|
+
coolify: this.backupGate.coolify,
|
|
414
|
+
orgId,
|
|
415
|
+
recordedIds: (await this.store.getAppliedState(orgId))?.serviceIds ?? {},
|
|
416
|
+
now: () => this.now().getTime(),
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
if (e instanceof UpgradeBackupMissingError) {
|
|
421
|
+
await this.refuse(plan, `upgrade of ${e.service} without a fresh backup`);
|
|
422
|
+
}
|
|
423
|
+
throw e;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// A move to the company's own network deletes its databases: a fresh dump of each first.
|
|
427
|
+
if (detail.plannedSteps.some((s) => s.action.system === 'coolify' && s.action.kind === 'network')) {
|
|
428
|
+
try {
|
|
429
|
+
if (!this.backupGate)
|
|
430
|
+
throw new NetworkMoveBackupMissingError('no-gate');
|
|
431
|
+
await requireFreshBackupsForMove({
|
|
432
|
+
coolify: this.backupGate.coolify,
|
|
433
|
+
projectUuid: (await this.store.getAppliedState(orgId))?.serviceIds
|
|
434
|
+
?.coolify_project,
|
|
435
|
+
ownsMail: ownsServices(org) && org.mail.mode !== 'off',
|
|
436
|
+
now: () => this.now().getTime(),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
if (e instanceof NetworkMoveBackupMissingError) {
|
|
441
|
+
await this.refuse(plan, 'network move without a fresh backup');
|
|
442
|
+
}
|
|
443
|
+
throw e;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
397
446
|
if (planNeedsConfirmation(plan.steps) &&
|
|
398
447
|
confirmedOrgName.trim() !== org.displayName.trim()) {
|
|
399
448
|
await this.refuse(plan, "destructive or migrating step without the org's name typed correctly");
|
|
400
449
|
throw new ConfirmationRequiredError(orgId);
|
|
401
450
|
}
|
|
402
|
-
const applied = await this.runSteps(org, plan, detail, fence);
|
|
451
|
+
const applied = await this.runSteps(org, plan, detail, fence, backupId);
|
|
403
452
|
await this.refreshPlatformFilesShare();
|
|
404
453
|
return applied;
|
|
405
454
|
}));
|
|
@@ -494,6 +543,27 @@ export class FoundryPlanApply {
|
|
|
494
543
|
await this.refusePlatform(plan, platform.ownerOrgId, `version change without confirmUpgrade naming: ${unconfirmed.map((u) => u.service).join(', ')}`);
|
|
495
544
|
throw new UpgradeConfirmationRequiredError(unconfirmed);
|
|
496
545
|
}
|
|
546
|
+
// F6: before step one. No override. The platform records no applied versions, so no backup id is kept.
|
|
547
|
+
if (this.backupGate) {
|
|
548
|
+
try {
|
|
549
|
+
await requireFreshBackupsForUpgrades(freshPlan, options.confirmUpgrade, {
|
|
550
|
+
coolify: this.backupGate.coolify,
|
|
551
|
+
orgId: platform.ownerOrgId,
|
|
552
|
+
recordedIds: await this.requirePlatformDeps().store.getServiceIds(),
|
|
553
|
+
now: () => this.now().getTime(),
|
|
554
|
+
scope: {
|
|
555
|
+
keys: PLATFORM_BACKUP_DATABASE_SCOPE_KEYS,
|
|
556
|
+
forService: platformDatabasesForService,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
catch (e) {
|
|
561
|
+
if (e instanceof UpgradeBackupMissingError) {
|
|
562
|
+
await this.refusePlatform(plan, platform.ownerOrgId, `upgrade of ${e.service} without a fresh backup`);
|
|
563
|
+
}
|
|
564
|
+
throw e;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
497
567
|
if (planNeedsConfirmation(plan.steps) && confirmedName.trim() !== 'platform') {
|
|
498
568
|
await this.refusePlatform(plan, platform.ownerOrgId, "destructive or migrating step without 'platform' typed correctly");
|
|
499
569
|
throw new PlatformConfirmationRequiredError();
|
|
@@ -587,6 +657,11 @@ export class FoundryPlanApply {
|
|
|
587
657
|
// gone" itself.
|
|
588
658
|
await this.destroyer.destroy(target, { org, recordedIds: { ...serviceIds } });
|
|
589
659
|
delete serviceIds[destroyServiceIdKey(target)];
|
|
660
|
+
if (target.system === 'cloudflare' && target.kind === 'tunnel') {
|
|
661
|
+
// The tunnel is gone, so what was written into it is too.
|
|
662
|
+
delete serviceIds[CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY];
|
|
663
|
+
delete serviceIds[CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY];
|
|
664
|
+
}
|
|
590
665
|
if (target.system === 'cloudflare' && target.kind === 'worker') {
|
|
591
666
|
// #318: its partial-deploy traces too, or a retry after a later
|
|
592
667
|
// step fails would read this Worker as live again.
|
|
@@ -807,15 +882,17 @@ export class FoundryPlanApply {
|
|
|
807
882
|
at: this.now(),
|
|
808
883
|
});
|
|
809
884
|
}
|
|
810
|
-
async runSteps(org, plan, detail, fence) {
|
|
885
|
+
async runSteps(org, plan, detail, fence, backupId = null) {
|
|
811
886
|
const orgId = plan.orgId;
|
|
812
887
|
const previous = await this.store.getAppliedState(orgId);
|
|
813
888
|
const serviceIds = { ...(previous?.serviceIds ?? {}) };
|
|
814
889
|
const checkpoints = [];
|
|
815
890
|
// Backed by the very `serviceIds` map this method persists once, at the
|
|
816
891
|
// end — see `StepExecutionContext`'s doc comment for why.
|
|
892
|
+
const ownerCompany = (await this.platformDeps?.store.get())?.ownerOrgId === orgId;
|
|
817
893
|
const context = {
|
|
818
894
|
org,
|
|
895
|
+
ownerCompany,
|
|
819
896
|
recordedIds: () => ({ ...serviceIds }),
|
|
820
897
|
now: () => this.now().getTime(),
|
|
821
898
|
recordScopeId: (key, id) => {
|
|
@@ -902,6 +979,7 @@ export class FoundryPlanApply {
|
|
|
902
979
|
service: orgId,
|
|
903
980
|
ref: lastApplied.declaredOrgVersion,
|
|
904
981
|
appliedAt: lastApplied.at,
|
|
982
|
+
backupId,
|
|
905
983
|
});
|
|
906
984
|
await this.audit.recordApply({
|
|
907
985
|
actor: this.actor,
|
|
@@ -942,7 +1020,7 @@ export class FoundryPlanApply {
|
|
|
942
1020
|
const platformHostnames = await this.platformServiceHostnames();
|
|
943
1021
|
const recorded = applied?.serviceIds ?? {};
|
|
944
1022
|
// Read only for an org Agora may switch and whose login host is applied.
|
|
945
|
-
const issuerHost = agoraIssuerHostFor(org
|
|
1023
|
+
const issuerHost = agoraIssuerHostFor(org).host;
|
|
946
1024
|
const loginHostServesDiscovery = issuerHost !== null && loginHostServesAgora(issuerHost, org, recorded)
|
|
947
1025
|
? await readLoginHostDiscovery(this.loginHostDiscovery, issuerHost)
|
|
948
1026
|
: false;
|
|
@@ -952,7 +1030,9 @@ export class FoundryPlanApply {
|
|
|
952
1030
|
loginHostBindings: this.loginHostBindings,
|
|
953
1031
|
loginHostServesDiscovery,
|
|
954
1032
|
zitadelIdTokenUserinfo: this.zitadelIdTokenUserinfo,
|
|
955
|
-
broker: this.
|
|
1033
|
+
broker: ownsServices(org) && this.hostOwnServices && this.executor.ownBrokerPort
|
|
1034
|
+
? await this.executor.ownBrokerPort(org, recorded)
|
|
1035
|
+
: this.broker,
|
|
956
1036
|
edgeWorkers: this.edgeWorkers,
|
|
957
1037
|
ownerInviteMail: this.ownerInviteMail,
|
|
958
1038
|
otherSlugs,
|
|
@@ -960,17 +1040,39 @@ export class FoundryPlanApply {
|
|
|
960
1040
|
archonUrl,
|
|
961
1041
|
platformHostnames,
|
|
962
1042
|
ownServices: this.hostOwnServices,
|
|
1043
|
+
// The `oikos.host` zone, its wildcard and its tunnel are the platform's `platform:oikos-ingress` step.
|
|
1044
|
+
oikosZoneReady: platformReadiness.oikosIngressApplied === true,
|
|
963
1045
|
ownMailKeyHeld: await this.ownMailKeyHeld(org),
|
|
964
1046
|
ownMailKeyMintable: this.executor.ownMailKeyMintable?.() ?? false,
|
|
1047
|
+
...(this.backupGate
|
|
1048
|
+
? {
|
|
1049
|
+
backupStorageUuid: (await this.platformDeps?.store.getServiceIds())?.[PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY] || null,
|
|
1050
|
+
}
|
|
1051
|
+
: {}),
|
|
965
1052
|
ownJobsTokenHeld: resumable.jobs,
|
|
966
1053
|
ownSfuKeysHeld: resumable.sfu,
|
|
967
|
-
...(ownsServices(org)
|
|
1054
|
+
...(ownsServices(org)
|
|
1055
|
+
? {
|
|
1056
|
+
mailRelayApplied: await this.mailRelayApplied(),
|
|
1057
|
+
ownMailSmtpPort: await this.ownMailSmtpPort(org),
|
|
1058
|
+
mailRelayRoute: await this.mailRelayHoldsRoute(org),
|
|
1059
|
+
}
|
|
1060
|
+
: {}),
|
|
968
1061
|
sfuPortsOpen,
|
|
969
1062
|
ownFilesDeps: this.executor.ownFilesDeps?.(),
|
|
970
1063
|
...(ownsServices(org) && this.executor.ownFilesR2Ports
|
|
971
1064
|
? { ownFilesR2Ports: await this.executor.ownFilesR2Ports(org, recorded) }
|
|
972
1065
|
: {}),
|
|
973
1066
|
});
|
|
1067
|
+
if (!ownsServices(org)) {
|
|
1068
|
+
// No company runs the shared services; each runs its own.
|
|
1069
|
+
detail.plannedSteps = [];
|
|
1070
|
+
detail.versionChanges = [];
|
|
1071
|
+
detail.unmanaged.push({
|
|
1072
|
+
description: `${org.slug} has no services.own: the shared services are removed, so a company runs its own. Set services.own.`,
|
|
1073
|
+
key: 'services:shared-removed',
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
974
1076
|
if (ownsServices(org) && !this.hostOwnServices) {
|
|
975
1077
|
detail.unmanaged.push({
|
|
976
1078
|
description: `${org.slug} runs its own services, and this host has no ownServices port`,
|
|
@@ -1046,7 +1148,9 @@ export class FoundryPlanApply {
|
|
|
1046
1148
|
const held = this.platformDeps?.heldKeys;
|
|
1047
1149
|
const platform = await this.platformDeps?.store.get();
|
|
1048
1150
|
const readiness = orgPlatformReadinessFor(org, base, orgRecordedIds, held ? await heldOwnKeyRefs(held, org) : new Set());
|
|
1049
|
-
return platform
|
|
1151
|
+
return platform
|
|
1152
|
+
? { ...readiness, serviceGitRefs: serviceGitRefsOf(platform, org.versionPin) }
|
|
1153
|
+
: readiness;
|
|
1050
1154
|
}
|
|
1051
1155
|
/** An own org's Lettermint key is held. A resolver that throws counts as not held. */
|
|
1052
1156
|
async ownMailKeyHeld(org) {
|
|
@@ -1064,6 +1168,27 @@ export class FoundryPlanApply {
|
|
|
1064
1168
|
const heldRefs = await heldOwnKeyRefs(held, org);
|
|
1065
1169
|
return { jobs: heldRefs.has(refs.jobsAdminToken), sfu: heldRefs.has(refs.livekitKeys) };
|
|
1066
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* The host port an own org's Stalwart publishes for the relay: the one it already has recorded, else
|
|
1173
|
+
* the lowest port in the range no other org has recorded. Two plans made at the same moment could pick
|
|
1174
|
+
* the same port; the second Stalwart then fails to bind it and the step fails, with nothing recorded.
|
|
1175
|
+
*/
|
|
1176
|
+
async ownMailSmtpPort(org) {
|
|
1177
|
+
const own = (await this.store.getAppliedState(org.id))?.serviceIds?.[OWN_MAIL_SMTP_PORT_SCOPE_KEY];
|
|
1178
|
+
if (own && isOwnMailSmtpPort(Number(own)))
|
|
1179
|
+
return Number(own);
|
|
1180
|
+
const taken = new Set();
|
|
1181
|
+
for (const other of await this.store.list()) {
|
|
1182
|
+
const port = (await this.store.getAppliedState(other.id))?.serviceIds?.[OWN_MAIL_SMTP_PORT_SCOPE_KEY];
|
|
1183
|
+
if (port)
|
|
1184
|
+
taken.add(Number(port));
|
|
1185
|
+
}
|
|
1186
|
+
for (let port = OWN_MAIL_SMTP_PORT_BASE; port <= OWN_MAIL_SMTP_PORT_MAX; port += 1) {
|
|
1187
|
+
if (!taken.has(port))
|
|
1188
|
+
return port;
|
|
1189
|
+
}
|
|
1190
|
+
return OWN_MAIL_SMTP_PORT_MAX + 1;
|
|
1191
|
+
}
|
|
1067
1192
|
/** The relay is declared and has recorded the routes it last applied: port 25 reaches the mail servers. */
|
|
1068
1193
|
async mailRelayApplied() {
|
|
1069
1194
|
const platform = await this.platformDeps?.store.get();
|
|
@@ -1072,6 +1197,21 @@ export class FoundryPlanApply {
|
|
|
1072
1197
|
const recorded = await this.platformDeps?.store.getServiceIds();
|
|
1073
1198
|
return Boolean(recorded?.[PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY]);
|
|
1074
1199
|
}
|
|
1200
|
+
/**
|
|
1201
|
+
* The relay's last applied routes (a fingerprint, recorded when it deploys) are the routes the
|
|
1202
|
+
* orgs' recorded SMTP addresses give now, and one of them is this org's mail domain.
|
|
1203
|
+
*/
|
|
1204
|
+
async mailRelayHoldsRoute(org) {
|
|
1205
|
+
const platform = await this.platformDeps?.store.get();
|
|
1206
|
+
if (!platform?.mail.relay)
|
|
1207
|
+
return false;
|
|
1208
|
+
const { mailRoutes } = await this.mailRelayInputs();
|
|
1209
|
+
const domain = ownHostnames(org).mailDomain.toLowerCase();
|
|
1210
|
+
if (!mailRoutes.some((r) => r.domain.toLowerCase() === domain))
|
|
1211
|
+
return false;
|
|
1212
|
+
const recorded = await this.platformDeps?.store.getServiceIds();
|
|
1213
|
+
return recorded?.[PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY] === mailRelayFingerprint(mailRoutes);
|
|
1214
|
+
}
|
|
1075
1215
|
async sharedPlatformReadiness() {
|
|
1076
1216
|
if (!this.platformDeps)
|
|
1077
1217
|
return DEFAULT_ORG_PLATFORM_READINESS;
|
|
@@ -1097,8 +1237,13 @@ export class FoundryPlanApply {
|
|
|
1097
1237
|
catch {
|
|
1098
1238
|
// not held
|
|
1099
1239
|
}
|
|
1240
|
+
const ownerOrg = await this.store.get(platform.ownerOrgId);
|
|
1241
|
+
const ownerRecorded = ownerOrg
|
|
1242
|
+
? (await this.store.getAppliedState(ownerOrg.id))?.serviceIds
|
|
1243
|
+
: {};
|
|
1100
1244
|
return {
|
|
1101
1245
|
...readiness,
|
|
1246
|
+
platformOwnerUserId: platformOwnerUserIdFrom(ownerOrg, ownerRecorded ?? {}),
|
|
1102
1247
|
mailKeyHeld,
|
|
1103
1248
|
canHoldSecrets: typeof heldKeys?.put === 'function',
|
|
1104
1249
|
dnsBlocked: dnsBlockedReason(platform, recorded),
|
|
@@ -1166,52 +1311,55 @@ export class FoundryPlanApply {
|
|
|
1166
1311
|
const callsOrgSlugs = (await this.store.list())
|
|
1167
1312
|
.filter((org) => org.calls.enabled)
|
|
1168
1313
|
.map((org) => org.slug);
|
|
1169
|
-
const mailRelay = platform.mail.relay === true ? await this.mailRelayInputs(
|
|
1314
|
+
const mailRelay = platform.mail.relay === true ? await this.mailRelayInputs() : null;
|
|
1315
|
+
const owner = await this.platformOwnerInput(platform);
|
|
1170
1316
|
const detail = computePlatformPlanDetail(platform, live, {
|
|
1317
|
+
owner,
|
|
1171
1318
|
jobsObservable: readers.coolify !== undefined,
|
|
1172
1319
|
sfuBlocked,
|
|
1173
1320
|
callsOrgSlugs,
|
|
1174
1321
|
...(mailRelay ?? {}),
|
|
1175
1322
|
recordedIds: serviceIds,
|
|
1176
|
-
|
|
1177
|
-
|
|
1323
|
+
liveBuilds: await readLiveBuilds(readers.coolify, serviceIds),
|
|
1324
|
+
blocked: (await executor.blockedSteps?.(platform, serviceIds, owner)) ?? {},
|
|
1325
|
+
drifted: (await executor.driftedSteps?.(platform, serviceIds, owner)) ?? {},
|
|
1178
1326
|
});
|
|
1179
1327
|
return { platform, plan: toPlatformPlan(detail, platform.ownerOrgId), detail };
|
|
1180
1328
|
}
|
|
1329
|
+
/** The owner company's own sign-in and files host, read from its applied ids; `null` members until applied. */
|
|
1330
|
+
async platformOwnerInput(platform) {
|
|
1331
|
+
const owner = await this.store.get(platform.ownerOrgId);
|
|
1332
|
+
if (!owner)
|
|
1333
|
+
return { signIn: null, files: null };
|
|
1334
|
+
const recorded = (await this.store.getAppliedState(owner.id))?.serviceIds ?? {};
|
|
1335
|
+
return { signIn: ownerSignInFrom(owner, recorded), files: ownerFilesFrom(owner, recorded) };
|
|
1336
|
+
}
|
|
1181
1337
|
/**
|
|
1182
1338
|
* What the shared mail relay needs from the orgs. A route exists for an org
|
|
1183
1339
|
* that owns its services, serves mail on its own domain and has recorded its
|
|
1184
1340
|
* mail server's SMTP address (`mail_smtp_internal`, written by the company
|
|
1185
|
-
* mail step)
|
|
1186
|
-
*
|
|
1187
|
-
* that owns its services but has no mail server yet
|
|
1188
|
-
*
|
|
1341
|
+
* mail step), on its mail domain: its own domain, or `<slug>.oikos.host`.
|
|
1342
|
+
* There is no platform mail server: a domain with no route is refused. An org
|
|
1343
|
+
* that owns its services but has no mail server yet has no route, so the
|
|
1344
|
+
* relay refuses its mail until the server exists.
|
|
1189
1345
|
*/
|
|
1190
|
-
async mailRelayInputs(
|
|
1346
|
+
async mailRelayInputs() {
|
|
1191
1347
|
const candidates = [];
|
|
1192
|
-
const mailPlatformDomains = [platform.apex];
|
|
1193
1348
|
for (const org of await this.store.list()) {
|
|
1194
1349
|
if (org.mail.mode === 'off')
|
|
1195
1350
|
continue;
|
|
1196
|
-
|
|
1197
|
-
if (!org.domain || !org.mail.usesDomain) {
|
|
1198
|
-
if (!mailPlatformDomains.includes(NO_DOMAIN_MAIL_APEX)) {
|
|
1199
|
-
mailPlatformDomains.push(NO_DOMAIN_MAIL_APEX);
|
|
1200
|
-
}
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
if (!ownsServices(org)) {
|
|
1204
|
-
mailPlatformDomains.push(org.domain);
|
|
1351
|
+
if (!ownsServices(org))
|
|
1205
1352
|
continue;
|
|
1206
|
-
}
|
|
1207
1353
|
const recorded = (await this.store.getAppliedState(org.id))?.serviceIds ?? {};
|
|
1208
1354
|
const target = recorded[OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY];
|
|
1209
1355
|
if (recorded.coolify_platform_mail_app && target) {
|
|
1210
|
-
candidates.push({
|
|
1356
|
+
candidates.push({
|
|
1357
|
+
slug: org.slug,
|
|
1358
|
+
route: { domain: ownHostnames(org).mailDomain, target },
|
|
1359
|
+
});
|
|
1211
1360
|
}
|
|
1212
1361
|
}
|
|
1213
|
-
// A domain
|
|
1214
|
-
const platformSet = new Set(mailPlatformDomains.map((d) => d.toLowerCase()));
|
|
1362
|
+
// A domain two companies claim gets no route; the other routes stay.
|
|
1215
1363
|
const claims = new Map();
|
|
1216
1364
|
for (const { route } of candidates) {
|
|
1217
1365
|
const domain = route.domain.toLowerCase();
|
|
@@ -1221,17 +1369,14 @@ export class FoundryPlanApply {
|
|
|
1221
1369
|
const mailRoutesBlocked = [];
|
|
1222
1370
|
for (const { slug, route } of candidates) {
|
|
1223
1371
|
const domain = route.domain.toLowerCase();
|
|
1224
|
-
if (
|
|
1225
|
-
mailRoutesBlocked.push(`${slug}'s mail route for ${route.domain} is blocked, not planned: ${route.domain} is a platform mail domain`);
|
|
1226
|
-
}
|
|
1227
|
-
else if ((claims.get(domain) ?? 0) > 1) {
|
|
1372
|
+
if ((claims.get(domain) ?? 0) > 1) {
|
|
1228
1373
|
mailRoutesBlocked.push(`${slug}'s mail route for ${route.domain} is blocked, not planned: another company claims ${route.domain}`);
|
|
1229
1374
|
}
|
|
1230
1375
|
else {
|
|
1231
1376
|
mailRoutes.push(route);
|
|
1232
1377
|
}
|
|
1233
1378
|
}
|
|
1234
|
-
return { mailRoutes,
|
|
1379
|
+
return { mailRoutes, mailRoutesBlocked };
|
|
1235
1380
|
}
|
|
1236
1381
|
async sfuBlockedReason(platform) {
|
|
1237
1382
|
if (!platform.sfu.enabled)
|
|
@@ -1272,6 +1417,7 @@ export class FoundryPlanApply {
|
|
|
1272
1417
|
const serviceIds = { ...(await store.getServiceIds()) };
|
|
1273
1418
|
const context = {
|
|
1274
1419
|
platform,
|
|
1420
|
+
owner: await this.platformOwnerInput(platform),
|
|
1275
1421
|
recordedIds: () => ({ ...serviceIds }),
|
|
1276
1422
|
recordScopeId: (key, id) => {
|
|
1277
1423
|
serviceIds[key] = id;
|
|
@@ -4,6 +4,7 @@ import type { DeclaredPlatform } from '../platform.js';
|
|
|
4
4
|
import type { PlannedStep } from './diff.js';
|
|
5
5
|
import type { LiveResource } from './live.js';
|
|
6
6
|
import { type MailRoute, PLATFORM_MAIL_RELAY_KEY } from './mail-relay.js';
|
|
7
|
+
import type { OwnerFiles, OwnerSignIn, PlatformOwnerInput } from './platform-org.js';
|
|
7
8
|
/**
|
|
8
9
|
* The platform's own diff result, the {@link DeclaredPlatform} counterpart to
|
|
9
10
|
* `diff.ts`'s `PlanDetail` -- everything the same, minus the `orgId` an org's
|
|
@@ -185,11 +186,15 @@ export declare const PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY = "archon_applie
|
|
|
185
186
|
*/
|
|
186
187
|
export declare const PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY = "archon_applied_rolling_update";
|
|
187
188
|
/**
|
|
188
|
-
* The sign-in
|
|
189
|
-
*
|
|
190
|
-
*
|
|
189
|
+
* The owner company's sign-in hostname Archon was last registered under, and
|
|
190
|
+
* the owner's files host (`<internalUrl>|<audience>`, or `none`) its env was
|
|
191
|
+
* last written with. `planArchon` plans the step again when either differs
|
|
192
|
+
* from the owner's current ones (a marker never recorded reads as `none`).
|
|
191
193
|
*/
|
|
192
|
-
export declare
|
|
194
|
+
export declare const PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY = "archon_applied_auth_hostname";
|
|
195
|
+
export declare const PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY = "archon_applied_files";
|
|
196
|
+
/** The value {@link PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY} records for the owner's files host. */
|
|
197
|
+
export declare function archonFilesMarker(files: OwnerFiles | null | undefined): string;
|
|
193
198
|
/**
|
|
194
199
|
* #175: set to '1' before the step points the tunnel at `http_status:404`
|
|
195
200
|
* (Access app rebuild), cleared to '' once the real ingress is written back.
|
|
@@ -537,8 +542,68 @@ export declare function serviceDbEnvAppliedScopeKey(service: 'chat' | 'calls-ser
|
|
|
537
542
|
* made again under a new id, is repaired once.
|
|
538
543
|
*/
|
|
539
544
|
export declare function zitadelAppIdTokenUserinfoAppliedScopeKey(appKey: string): string;
|
|
540
|
-
/**
|
|
545
|
+
/**
|
|
546
|
+
* Archon's own sign-in organisation, in the owner company's ZITADEL instance
|
|
547
|
+
* and apart from the organisation that holds the company's people (and Boule's
|
|
548
|
+
* sign-in). Made by the `zitadel:archon-org` step, which runs before
|
|
549
|
+
* `platform:archon`. The first six keys below are the step's own progress:
|
|
550
|
+
* each is recorded only after its call succeeded.
|
|
551
|
+
*/
|
|
552
|
+
export declare const PLATFORM_ARCHON_ORG_SCOPE_KEY = "zitadel_platform_archon_org";
|
|
553
|
+
/** The primary domain registered on Archon's sign-in organisation. */
|
|
554
|
+
export declare const PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY = "zitadel_platform_archon_org_domain";
|
|
555
|
+
/** The domain the sign-in server refused to make primary; set instead of failing, reported as blocked. */
|
|
556
|
+
export declare const PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY = "zitadel_platform_archon_org_domain_blocked";
|
|
557
|
+
/** The Archon org id once self-registration is closed on it. */
|
|
558
|
+
export declare const PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY = "zitadel_platform_archon_org_registration_closed";
|
|
559
|
+
/** The Archon org id once its login policy forces a second factor. */
|
|
560
|
+
export declare const PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY = "zitadel_platform_archon_org_policy";
|
|
561
|
+
/** The first person's user id in the Archon org (`none` when no owner is declared), recorded once the invite was mailed. */
|
|
562
|
+
export declare const PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY = "zitadel_platform_archon_owner_invite";
|
|
563
|
+
/** The Archon org the recorded invite was made in. */
|
|
564
|
+
export declare const PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY = "zitadel_platform_archon_owner_invite_org";
|
|
565
|
+
/** The name of the Archon sign-in organisation: `<owner slug>-archon`. */
|
|
566
|
+
export declare function archonOrgName(ownerSlug: string): string;
|
|
567
|
+
/** The primary domain of the Archon sign-in organisation: `archon.` before the owner's own sign-in domain. */
|
|
568
|
+
export declare function archonOrgDomain(ownerSignInDomain: string): string;
|
|
569
|
+
/**
|
|
570
|
+
* The CI organisation id the last Archon apply wrote as `CI_ORGANIZATION_ID`,
|
|
571
|
+
* and the `ARCHON_FIRST_OWNER_LINK` value it wrote (empty when none).
|
|
572
|
+
* `planArchon` plans the step again when either differs from what is wanted.
|
|
573
|
+
*/
|
|
574
|
+
export declare const PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY = "archon_applied_ci_org";
|
|
575
|
+
export declare const PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY = "archon_applied_first_owner_link";
|
|
576
|
+
/**
|
|
577
|
+
* `<archonUserId>:<ciUserId>`: the first person in both organisations, which
|
|
578
|
+
* Archon seeds its link table from. Empty when either id is not known (no
|
|
579
|
+
* owner declared, or the CI-side account not recorded yet).
|
|
580
|
+
*/
|
|
581
|
+
export declare function archonFirstOwnerLink(recordedIds: Readonly<Record<string, string>>, ownerSignIn: OwnerSignIn | null): string;
|
|
582
|
+
/**
|
|
583
|
+
* The one-time-PIN provider id the Archon Access app was last made with.
|
|
584
|
+
* Drift is read against it, because a plan must not ask edge to find or make
|
|
585
|
+
* the provider.
|
|
586
|
+
*/
|
|
587
|
+
export declare const PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY = "cloudflare_platform_archon_access_pin";
|
|
588
|
+
/**
|
|
589
|
+
* The Archon org the `AUTH_APP_KEY` in Archon's env was minted for. A key
|
|
590
|
+
* minted for another org (the CI org, before the move) is replaced by a fresh
|
|
591
|
+
* one, so the key the CI-org broker entry held stops being Archon's.
|
|
592
|
+
*/
|
|
593
|
+
export declare const PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY = "archon_app_key_org";
|
|
594
|
+
/**
|
|
595
|
+
* `<org>|<project>|<app>` of Archon's old sign-in client while it still waits
|
|
596
|
+
* to be deleted. Set before the new client's ids overwrite the recorded ones,
|
|
597
|
+
* cleared once the old client is gone.
|
|
598
|
+
*/
|
|
599
|
+
export declare const PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY = "zitadel_platform_archon_retired_client";
|
|
600
|
+
/** The ZITADEL app id of Archon's sign-in client in the owner company's ZITADEL. */
|
|
541
601
|
export declare const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = "zitadel_platform_archon_app";
|
|
602
|
+
/** The ZITADEL project id Archon's sign-in client lives in. */
|
|
603
|
+
export declare const PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY = "zitadel_platform_archon_project";
|
|
604
|
+
/** The ZITADEL project and client ids of the Access sign-in provider, in the owner company's ZITADEL. */
|
|
605
|
+
export declare const PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY = "zitadel_platform_access_project";
|
|
606
|
+
export declare const PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY = "zitadel_platform_access_app";
|
|
542
607
|
/**
|
|
543
608
|
* The ZITADEL app id of Archon's sign-in client once "user info inside the
|
|
544
609
|
* ID token" was switched on for it. `planArchonIdTokenUserinfo` plans the
|
|
@@ -587,9 +652,8 @@ export declare function appDomainsAppliedScopeKey(entryKey: string): string;
|
|
|
587
652
|
* unknown, bare before 4b2380f and `https://` after it.
|
|
588
653
|
*/
|
|
589
654
|
export declare function appliedAppDomains(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, entryKey: string): string[];
|
|
590
|
-
/** Whether the secret sources can be filled right now:
|
|
655
|
+
/** Whether the secret sources can be filled right now: a new shared secret can be held. */
|
|
591
656
|
export interface AppSecretAvailability {
|
|
592
|
-
platformMailKey: boolean;
|
|
593
657
|
sharedSecret: boolean;
|
|
594
658
|
}
|
|
595
659
|
/**
|
|
@@ -608,7 +672,7 @@ export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, rec
|
|
|
608
672
|
unresolved: Array<[string, string]>;
|
|
609
673
|
secrets: Array<{
|
|
610
674
|
name: string;
|
|
611
|
-
source:
|
|
675
|
+
source: EnvSharedSecret;
|
|
612
676
|
}>;
|
|
613
677
|
unresolvedSecrets: Array<[string, string]>;
|
|
614
678
|
marker: Record<string, string>;
|
|
@@ -627,6 +691,13 @@ export declare const PLATFORM_AI_KEY = "platform:ai";
|
|
|
627
691
|
* public hostname. Holds whichever way the public-hostname decision goes.
|
|
628
692
|
*/
|
|
629
693
|
export declare const PLATFORM_AI_INTERNAL_URL_SCOPE_KEY = "ai_platform_internal_url";
|
|
694
|
+
/**
|
|
695
|
+
* The id of the tunnel that carries the AI host's public hostname. Recorded once the tunnel, its
|
|
696
|
+
* connector and the DNS record are in place; an entry is given `AI_URL` only after it.
|
|
697
|
+
*/
|
|
698
|
+
export declare const PLATFORM_AI_TUNNEL_SCOPE_KEY = "cloudflare_platform_ai_tunnel";
|
|
699
|
+
/** The one Coolify app that runs `cloudflared` for the AI tunnel. */
|
|
700
|
+
export declare const PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY = "coolify_platform_ai_connector_app";
|
|
630
701
|
/**
|
|
631
702
|
* #172: the same two-key discipline as the ZITADEL admin key above, for
|
|
632
703
|
* foundry's AI management key (the bearer key `ai:organisation`, `ai:budget`
|
|
@@ -833,6 +904,18 @@ export declare const PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY = "hc_applied_git_ref
|
|
|
833
904
|
* and that implementation is where the key is actually used.
|
|
834
905
|
*/
|
|
835
906
|
export declare const PLATFORM_HC_API_KEY_REF = "hc:platform:apiKey";
|
|
907
|
+
/**
|
|
908
|
+
* foundry#544: the held-key ref a human puts the *outside* Healthchecks
|
|
909
|
+
* account's project API key under, read only while
|
|
910
|
+
* `platform.heartbeatsProvider` is `'hosted'` -- {@link PLATFORM_HC_API_KEY_REF}'s
|
|
911
|
+
* own ref is for the self-hosted instance only, and the two are never
|
|
912
|
+
* conflated: switching provider needs its own key held under its own ref,
|
|
913
|
+
* never the other one silently reused. Same "this package never reads the
|
|
914
|
+
* value back" rule as {@link PLATFORM_HC_API_KEY_REF}'s own doc comment --
|
|
915
|
+
* whoever implements {@link HeartbeatsPort} against the outside service is
|
|
916
|
+
* the host.
|
|
917
|
+
*/
|
|
918
|
+
export declare const PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF = "heartbeats:platform:hostedApiKey";
|
|
836
919
|
/**
|
|
837
920
|
* #540: the heartbeat check named after the uptime Worker (foundry#542,
|
|
838
921
|
* which is what actually pings it -- blocked today on edge#26, so nothing
|
|
@@ -989,16 +1072,14 @@ export interface PlanPlatformOptions {
|
|
|
989
1072
|
* `platform.mail.relay` is true (default `[]`).
|
|
990
1073
|
*/
|
|
991
1074
|
mailRoutes?: readonly MailRoute[];
|
|
992
|
-
/**
|
|
993
|
-
* The domains the platform's own Stalwart serves; the relay hands them to
|
|
994
|
-
* it and refuses any domain in neither list. Used only when
|
|
995
|
-
* `platform.mail.relay` is true (default `[]`).
|
|
996
|
-
*/
|
|
997
|
-
mailPlatformDomains?: readonly string[];
|
|
998
1075
|
/** One line per company route left out of `mailRoutes`; each is reported blocked and the relay still plans without it. */
|
|
999
1076
|
mailRoutesBlocked?: readonly string[];
|
|
1077
|
+
/** The owner company's own sign-in and files host; Archon and the Access provider read them (default: neither applied). */
|
|
1078
|
+
owner?: PlatformOwnerInput;
|
|
1000
1079
|
/** The platform's recorded resource/scope ids (default `{}`). */
|
|
1001
1080
|
recordedIds?: Readonly<Record<string, string>>;
|
|
1081
|
+
/** The commit each recorded app runs now, by app name; a differing one gives a `build-drift:<name>` note (default `{}`). */
|
|
1082
|
+
liveBuilds?: Readonly<Record<string, string>>;
|
|
1002
1083
|
/** Per-step block reasons the executor reports (default `{}`). */
|
|
1003
1084
|
blocked?: {
|
|
1004
1085
|
auth?: string;
|
|
@@ -1032,6 +1113,8 @@ export interface PlanPlatformOptions {
|
|
|
1032
1113
|
edge?: string;
|
|
1033
1114
|
/** #604: why the Access sign-in provider cannot be made yet (no dependencies, or edge cannot confirm Access is ready), or absent. */
|
|
1034
1115
|
accessProvider?: string;
|
|
1116
|
+
/** Why the shared oikos.host ingress cannot be made (no dependencies, or its zone is not on edge's account), or absent. */
|
|
1117
|
+
oikosIngress?: string;
|
|
1035
1118
|
/**
|
|
1036
1119
|
* #257: true once a service's database exists but the host built the
|
|
1037
1120
|
* platform executor with no backup dependencies -- every such service's
|
|
@@ -1095,6 +1178,10 @@ export interface PlanPlatformOptions {
|
|
|
1095
1178
|
* step is not planned.
|
|
1096
1179
|
*/
|
|
1097
1180
|
authSignInPolicy?: boolean;
|
|
1181
|
+
/** Why Archon's own sign-in organisation cannot be made (no dependencies, no ZITADEL client for the instance), or absent. */
|
|
1182
|
+
archonOrg?: string;
|
|
1183
|
+
/** Why the first person cannot be invited into Archon's organisation yet (no work address, no mail port). The rest of the step still plans. */
|
|
1184
|
+
archonOrgInvite?: string;
|
|
1098
1185
|
};
|
|
1099
1186
|
/**
|
|
1100
1187
|
* Steps whose live state differs from the declared one in a way the recorded
|
|
@@ -1189,11 +1276,8 @@ export declare const JOBS_INTERNAL_URL_SCOPE_KEY = "jobs_internal_url";
|
|
|
1189
1276
|
* never a wrong step.
|
|
1190
1277
|
*/
|
|
1191
1278
|
export declare const PLATFORM_BACKUP_DATABASE_SCOPE_KEYS: {
|
|
1192
|
-
readonly jobs: "coolify_jobs_database";
|
|
1193
|
-
readonly auth: "coolify_platform_auth_database";
|
|
1194
1279
|
readonly archon: "coolify_platform_archon_database";
|
|
1195
1280
|
readonly ai: "coolify_platform_ai_database";
|
|
1196
|
-
readonly files: "coolify_platform_files_database";
|
|
1197
1281
|
readonly integrations: "coolify_integrations_database";
|
|
1198
1282
|
};
|
|
1199
1283
|
export type PlatformBackupServiceKey = keyof typeof PLATFORM_BACKUP_DATABASE_SCOPE_KEYS;
|
|
@@ -1265,3 +1349,35 @@ export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = "in
|
|
|
1265
1349
|
* `''`) as soon as the mint is recorded, so one request is one rotation.
|
|
1266
1350
|
*/
|
|
1267
1351
|
export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = "integrations_management_key_rotation_requested";
|
|
1352
|
+
/**
|
|
1353
|
+
* #175: Archon (https://github.com/wtfalch/archon), the one shared console,
|
|
1354
|
+
* built fresh at `platform.archon.hostname` (`archon.<apex>`) rather than the
|
|
1355
|
+
* hand-deployed `operator` app it replaces (#165). One step,
|
|
1356
|
+
* `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, covers the app, its
|
|
1357
|
+
* database, its Cloudflare tunnel, Access app and DNS record, and its sign-in
|
|
1358
|
+
* client in the platform's own ZITADEL -- the same bundling {@link planAuth}
|
|
1359
|
+
* does, for the same reason.
|
|
1360
|
+
*
|
|
1361
|
+
* Idempotent on the recorded resource id plus the applied hostname and
|
|
1362
|
+
* `gitRef`, for the reason {@link planAuth} gives: no platform reader reports
|
|
1363
|
+
* these resources, so a live check would replan the step forever. A changed
|
|
1364
|
+
* hostname or `gitRef` plans the step again, and so does `drifted` (the Access
|
|
1365
|
+
* policy's include list is not the declared one). A re-run mints nothing.
|
|
1366
|
+
*
|
|
1367
|
+
* Blocked, not planned, while the platform's sign-in is neither recorded nor
|
|
1368
|
+
* planned in this same plan: Archon's sign-in client is created inside it, so
|
|
1369
|
+
* the step could only throw.
|
|
1370
|
+
*
|
|
1371
|
+
* #213: also puts {@link PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF},
|
|
1372
|
+
* {@link PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF} and
|
|
1373
|
+
* {@link PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF} as held keys, before
|
|
1374
|
+
* the app deploys -- `putArchonMasterKeys` in `platform-providers.ts`.
|
|
1375
|
+
*
|
|
1376
|
+
* #197: a recorded app with no recorded `cloudflared` connector
|
|
1377
|
+
* ({@link PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY}) plans the step again, as
|
|
1378
|
+
* {@link planAuth} does.
|
|
1379
|
+
*/
|
|
1380
|
+
/** Archon signs its operators in through the owner company's own sign-in; fixed text, part of the stale-plan comparison. */
|
|
1381
|
+
export declare const ARCHON_OWNER_SIGN_IN_BLOCKED = "Archon is blocked, not planned: the owner company's sign-in is not applied yet";
|
|
1382
|
+
/** Archon signs in from its own organisation, which the `zitadel:archon-org` step makes; fixed text, part of the stale-plan comparison. */
|
|
1383
|
+
export declare const ARCHON_ORG_BLOCKED = "Archon is blocked, not planned: its own sign-in organisation is not applied yet";
|