@wtfalch/foundry 0.34.0 → 0.35.2
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 +799 -241
- 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
|
@@ -47,7 +47,7 @@ export interface BootstrapCompanyDeps {
|
|
|
47
47
|
readers: LiveReaders;
|
|
48
48
|
executor: StepExecutor;
|
|
49
49
|
/** The same flags `FoundryPlanApply` takes: a step whose port the host did not build is reported blocked. */
|
|
50
|
-
options?: Pick<FoundryPlanApplyOptions, 'ownerInviteMail' | 'broker'>;
|
|
50
|
+
options?: Pick<FoundryPlanApplyOptions, 'ownerInviteMail' | 'broker' | 'ownServices'>;
|
|
51
51
|
}
|
|
52
52
|
export interface BootstrapPlatformDeps {
|
|
53
53
|
/** Foundry's tables live here, already migrated by the host; the store, the audit log and the plan all use it. */
|
|
@@ -59,13 +59,18 @@ function allowBootstrapApply(withCompany) {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
* The org steps phase two applies: the sign-in
|
|
62
|
+
* The org steps phase two applies: the owner's own sign-in server and its zone,
|
|
63
|
+
* the sign-in org and its domain, the owner
|
|
63
64
|
* (whose user id Boule's `AUTHZ_FIRST_OWNER` carries), Boule's sign-in client,
|
|
64
65
|
* and Boule with its database. Everything else the company declares (chat,
|
|
65
66
|
* mail, Boule's broker entry, which waits for the org's mail domain, ...) is
|
|
66
67
|
* for a later, ordinary org apply.
|
|
67
68
|
*/
|
|
68
69
|
const COMPANY_SLICE_STEPS = new Set([
|
|
70
|
+
// The owner's own sign-in server, before anyone can sign in. Own mail, files,
|
|
71
|
+
// jobs and sfu follow after sign-in, as for every company.
|
|
72
|
+
'cloudflare:zone',
|
|
73
|
+
'coolify:own-auth',
|
|
69
74
|
'zitadel:issuer',
|
|
70
75
|
'zitadel:registration-closed',
|
|
71
76
|
'zitadel:domain',
|
|
@@ -77,8 +82,10 @@ const companySliceOnly = {
|
|
|
77
82
|
step: (action) => COMPANY_SLICE_STEPS.has(`${action.system}:${action.kind}`) ||
|
|
78
83
|
COMPANY_SLICE_STEPS.has(`${action.system}:${action.kind}:${action.key}`),
|
|
79
84
|
// `coolify:boule:mail` only says Boule runs without mail for now.
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
// `own-auth` is the unmanaged key `planOwnAuth` uses for a blocked sign-in server.
|
|
86
|
+
blocked: (key) => key === 'own-auth' ||
|
|
87
|
+
(key !== 'coolify:boule:mail' &&
|
|
88
|
+
[...COMPANY_SLICE_STEPS].some((step) => key === step || key.startsWith(`${step}:`))),
|
|
82
89
|
};
|
|
83
90
|
/** Shown instead of a phase-two error that could carry a value; the failing step's own name and redacted text are in `failed`. */
|
|
84
91
|
const COMPANY_NOT_FINISHED = "the company's own slice did not finish, so Archon has no people-store keys yet; run onboarding again";
|
|
@@ -118,6 +125,10 @@ function trackedOrgExecutor(inner, progress, redactor) {
|
|
|
118
125
|
}
|
|
119
126
|
},
|
|
120
127
|
...(inner.ownEdgePorts ? { ownEdgePorts: () => inner.ownEdgePorts?.() ?? {} } : {}),
|
|
128
|
+
// The own sign-in step plans without a held Lettermint key only when the executor can mint one.
|
|
129
|
+
...(inner.ownMailKeyMintable
|
|
130
|
+
? { ownMailKeyMintable: () => inner.ownMailKeyMintable?.() ?? false }
|
|
131
|
+
: {}),
|
|
121
132
|
};
|
|
122
133
|
}
|
|
123
134
|
/** The audit log with every free-text field run through the redactor before it is stored. */
|
|
@@ -334,10 +345,14 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
334
345
|
return result;
|
|
335
346
|
},
|
|
336
347
|
...(inner.blockedSteps
|
|
337
|
-
? {
|
|
348
|
+
? {
|
|
349
|
+
blockedSteps: (platform, ids, owner) => inner.blockedSteps?.(platform, ids, owner) ?? {},
|
|
350
|
+
}
|
|
338
351
|
: {}),
|
|
339
352
|
...(inner.driftedSteps
|
|
340
|
-
? {
|
|
353
|
+
? {
|
|
354
|
+
driftedSteps: (platform, ids, owner) => inner.driftedSteps?.(platform, ids, owner) ?? {},
|
|
355
|
+
}
|
|
341
356
|
: {}),
|
|
342
357
|
};
|
|
343
358
|
let orgProgress = { started: 0, completed: 0 };
|
|
@@ -392,10 +407,11 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
392
407
|
const blockedRaw = (driven.failed ? [] : (driven.lastPlan?.unmanaged ?? []))
|
|
393
408
|
.filter((item) => item.key !== undefined)
|
|
394
409
|
.map((item) => ({ step: item.key, message: item.description }));
|
|
410
|
+
const platformBlockedCount = blockedRaw.length;
|
|
395
411
|
if (companyDeps && !failed) {
|
|
396
412
|
// Phase two: the platform's own org. Its Boule needs the platform's
|
|
397
413
|
// sign-in, and Archon's people-store keys need that Boule.
|
|
398
|
-
const
|
|
414
|
+
const driveCompany = (label) => driveApply({
|
|
399
415
|
plan: () => planApply.plan(COMPANY_INCLUDED_ORG_ID),
|
|
400
416
|
apply: (plan, stepProgress) => {
|
|
401
417
|
orgProgress = stepProgress;
|
|
@@ -406,27 +422,30 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
406
422
|
phase: 'plan',
|
|
407
423
|
system: roundSystem(plan),
|
|
408
424
|
round,
|
|
409
|
-
message:
|
|
425
|
+
message: `${label}company round ${round}: ${plan.steps.length} step(s) planned`,
|
|
410
426
|
steps: plan.steps.map((s) => s.description),
|
|
411
427
|
}),
|
|
412
428
|
onApplied: (round, plan) => emit({
|
|
413
429
|
phase: 'apply',
|
|
414
430
|
system: roundSystem(plan),
|
|
415
431
|
round,
|
|
416
|
-
message:
|
|
432
|
+
message: `${label}company round ${round}: applied`,
|
|
417
433
|
steps: plan.steps.map((s) => s.description),
|
|
418
434
|
}),
|
|
419
435
|
});
|
|
436
|
+
const company = await driveCompany('');
|
|
420
437
|
applied.push(...company.applied);
|
|
421
438
|
if (company.failed) {
|
|
422
439
|
failed = company.failed;
|
|
423
440
|
blockedRaw.push({ step: 'company', message: COMPANY_NOT_FINISHED });
|
|
424
441
|
}
|
|
425
442
|
else {
|
|
443
|
+
const companyBlockedStart = blockedRaw.length;
|
|
426
444
|
for (const item of company.lastPlan?.unmanaged ?? []) {
|
|
427
445
|
if (item.key !== undefined)
|
|
428
446
|
blockedRaw.push({ step: item.key, message: item.description });
|
|
429
447
|
}
|
|
448
|
+
const companyBlockedCount = blockedRaw.length - companyBlockedStart;
|
|
430
449
|
// Boule's broker entry needs the org's mail domain, which only the org's mail step
|
|
431
450
|
// records, so it is outside this slice. Until it is applied nobody can sign in to Boule:
|
|
432
451
|
// say so, and never report the onboarding complete.
|
|
@@ -444,12 +463,31 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
444
463
|
failed = again.failed;
|
|
445
464
|
lastPlan = again.lastPlan;
|
|
446
465
|
if (!again.failed) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
466
|
+
// Archon's registration put the first entry on the owner's broker. Only now can the
|
|
467
|
+
// owner's sign-in mint its login client and start that broker, so its plan runs once more.
|
|
468
|
+
const signIn = await driveCompany('after Archon: ');
|
|
469
|
+
applied.push(...signIn.applied);
|
|
470
|
+
if (signIn.failed) {
|
|
471
|
+
failed = signIn.failed;
|
|
472
|
+
blockedRaw.push({ step: 'company', message: COMPANY_NOT_FINISHED });
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const fresh = [];
|
|
476
|
+
for (const item of signIn.lastPlan?.unmanaged ?? []) {
|
|
477
|
+
if (item.key !== undefined)
|
|
478
|
+
fresh.push({ step: item.key, message: item.description });
|
|
450
479
|
}
|
|
480
|
+
blockedRaw.splice(companyBlockedStart, companyBlockedCount, ...fresh);
|
|
451
481
|
}
|
|
452
482
|
}
|
|
483
|
+
if (!failed) {
|
|
484
|
+
// Archon was blocked on the owner's sign-in in phase one; this plan is the current truth.
|
|
485
|
+
blockedRaw.splice(0, platformBlockedCount);
|
|
486
|
+
const platformBlocked = (again.lastPlan?.unmanaged ?? [])
|
|
487
|
+
.filter((item) => item.key !== undefined)
|
|
488
|
+
.map((item) => ({ step: item.key, message: item.description }));
|
|
489
|
+
blockedRaw.unshift(...platformBlocked);
|
|
490
|
+
}
|
|
453
491
|
}
|
|
454
492
|
}
|
|
455
493
|
const blocked = blockedRaw.map((item) => ({
|
|
@@ -38,6 +38,11 @@ export interface BootstrapCompany {
|
|
|
38
38
|
name: string;
|
|
39
39
|
slug: string;
|
|
40
40
|
apex: string;
|
|
41
|
+
/**
|
|
42
|
+
* The owner company's own domain: absent means `apex`, `null` means none (it
|
|
43
|
+
* then runs on oikos.host). `apex` stays the platform's.
|
|
44
|
+
*/
|
|
45
|
+
domain?: string | null;
|
|
41
46
|
}
|
|
42
47
|
export interface BootstrapOwner {
|
|
43
48
|
givenName: string;
|
|
@@ -54,13 +59,15 @@ export interface BootstrapOwner {
|
|
|
54
59
|
*/
|
|
55
60
|
export declare function ownerIdentity(owner: BootstrapOwner): OwnerIdentity;
|
|
56
61
|
/**
|
|
57
|
-
* Company Included's declared org: the platform's owner,
|
|
58
|
-
*
|
|
59
|
-
*
|
|
62
|
+
* Company Included's declared org: the platform's owner, a company like any
|
|
63
|
+
* other (`services.own`: its own sign-in, mail, files and jobs). Tier 3 on the
|
|
64
|
+
* platform's apex from the start (#512) -- it runs on that domain with
|
|
65
|
+
* hosted mail, not a level-1 org whose domain gets set by hand afterward --
|
|
66
|
+
* unless `company.domain` is `null`, which runs it on oikos.host.
|
|
60
67
|
* `issuer`/`calls`/`site` go on the domain too, the same tier-3 defaults
|
|
61
68
|
* archon#388 gives every company declared there. `calls` and `site` stay
|
|
62
|
-
* disabled (calls is not in v1; no site is declared)
|
|
63
|
-
*
|
|
69
|
+
* disabled (calls is not in v1; no site is declared). Without a domain
|
|
70
|
+
* `calls.usesDomain` is off, as `validateCalls` requires. Chat stays off the domain: its
|
|
64
71
|
* re-key onto a per-org homeserver is not built yet (#463). The org's own
|
|
65
72
|
* sign-in still never collides with the shared ZITADEL instance's host --
|
|
66
73
|
* `loginHostnameFor` (#488) falls back to `<slug>-auth.oikos.host` whenever
|
|
@@ -36,10 +36,11 @@ export const DEFAULT_MAIL_IMAGE_TAG = VERSIONS.stalwart;
|
|
|
36
36
|
*/
|
|
37
37
|
export const DEFAULT_HC_IMAGE_TAG = VERSIONS.hc;
|
|
38
38
|
/**
|
|
39
|
-
* Every platform service
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* Every platform service is enabled except `auth`, `mail`, `files`, `jobs`
|
|
40
|
+
* and `sfu`: the owner company runs its own copies of those like any
|
|
41
|
+
* company (`validatePlatform` refuses them on), so the platform keeps only
|
|
42
|
+
* what no company owns. A service whose dependencies the host did not supply
|
|
43
|
+
* is reported blocked, not planned. `edge` is on because every
|
|
43
44
|
* platform DNS record is written through it (F4.dns); its own Cloudflare API
|
|
44
45
|
* token is a held key (`cloudflare:edge:apiToken`), and until a human holds
|
|
45
46
|
* it the planner reports edge blocked, and auth, mail and archon with it.
|
|
@@ -47,16 +48,23 @@ export const DEFAULT_HC_IMAGE_TAG = VERSIONS.hc;
|
|
|
47
48
|
* the same as mail and archon.
|
|
48
49
|
*/
|
|
49
50
|
const SERVICES_ENABLED_BY_DEFAULT = new Set([
|
|
50
|
-
'auth',
|
|
51
|
-
'mail',
|
|
52
|
-
'files',
|
|
53
51
|
'ai',
|
|
54
|
-
'jobs',
|
|
55
52
|
'integrations',
|
|
56
53
|
'archon',
|
|
57
54
|
'edge',
|
|
58
55
|
'hc',
|
|
59
56
|
]);
|
|
57
|
+
/**
|
|
58
|
+
* The services every company deploys its own copy of. Their platform entries
|
|
59
|
+
* are off but still carry the release the own steps deploy
|
|
60
|
+
* (`serviceGitRefsOf`); with an empty one `own-auth` is blocked.
|
|
61
|
+
*/
|
|
62
|
+
const OWN_SERVICE_KEYS = new Set([
|
|
63
|
+
'auth',
|
|
64
|
+
'mail',
|
|
65
|
+
'files',
|
|
66
|
+
'jobs',
|
|
67
|
+
]);
|
|
60
68
|
/**
|
|
61
69
|
* The owner as `OwnerIdentity`, trimmed. Throws a plain error naming the
|
|
62
70
|
* field when one is empty or an address is not an address. The work address
|
|
@@ -75,33 +83,36 @@ export function ownerIdentity(owner) {
|
|
|
75
83
|
return identity;
|
|
76
84
|
}
|
|
77
85
|
/**
|
|
78
|
-
* Company Included's declared org: the platform's owner,
|
|
79
|
-
*
|
|
80
|
-
*
|
|
86
|
+
* Company Included's declared org: the platform's owner, a company like any
|
|
87
|
+
* other (`services.own`: its own sign-in, mail, files and jobs). Tier 3 on the
|
|
88
|
+
* platform's apex from the start (#512) -- it runs on that domain with
|
|
89
|
+
* hosted mail, not a level-1 org whose domain gets set by hand afterward --
|
|
90
|
+
* unless `company.domain` is `null`, which runs it on oikos.host.
|
|
81
91
|
* `issuer`/`calls`/`site` go on the domain too, the same tier-3 defaults
|
|
82
92
|
* archon#388 gives every company declared there. `calls` and `site` stay
|
|
83
|
-
* disabled (calls is not in v1; no site is declared)
|
|
84
|
-
*
|
|
93
|
+
* disabled (calls is not in v1; no site is declared). Without a domain
|
|
94
|
+
* `calls.usesDomain` is off, as `validateCalls` requires. Chat stays off the domain: its
|
|
85
95
|
* re-key onto a per-org homeserver is not built yet (#463). The org's own
|
|
86
96
|
* sign-in still never collides with the shared ZITADEL instance's host --
|
|
87
97
|
* `loginHostnameFor` (#488) falls back to `<slug>-auth.oikos.host` whenever
|
|
88
98
|
* `auth.<domain>` would equal the platform's own auth hostname.
|
|
89
99
|
*/
|
|
90
100
|
export function declaredCompanyOrg(company, owner) {
|
|
101
|
+
const domain = company.domain === undefined ? company.apex : company.domain;
|
|
91
102
|
return {
|
|
92
103
|
id: COMPANY_INCLUDED_ORG_ID,
|
|
93
104
|
slug: company.slug,
|
|
94
105
|
displayName: company.name,
|
|
95
|
-
domain
|
|
106
|
+
domain,
|
|
96
107
|
mail: {
|
|
97
108
|
mode: 'hosted',
|
|
98
|
-
usesDomain:
|
|
109
|
+
usesDomain: domain !== null,
|
|
99
110
|
ownerMailboxAddress: null,
|
|
100
111
|
noReplyLocalPart: 'no-reply',
|
|
101
112
|
},
|
|
102
113
|
chat: { enabled: true, usesDomain: false },
|
|
103
|
-
calls: { enabled: false, usesDomain:
|
|
104
|
-
issuer: { usesDomain:
|
|
114
|
+
calls: { enabled: false, usesDomain: domain !== null },
|
|
115
|
+
issuer: { usesDomain: domain !== null },
|
|
105
116
|
site: { enabled: false, usesDomain: true },
|
|
106
117
|
hosting: {
|
|
107
118
|
enabled: false,
|
|
@@ -119,6 +130,7 @@ export function declaredCompanyOrg(company, owner) {
|
|
|
119
130
|
buyer: { legalName: null, orgNumber: null, address: null, vatRegistered: null },
|
|
120
131
|
bespokeAppRepo: null,
|
|
121
132
|
adoptedApp: null,
|
|
133
|
+
services: { own: true },
|
|
122
134
|
owners: [ownerIdentity(owner)],
|
|
123
135
|
test: false,
|
|
124
136
|
};
|
|
@@ -134,7 +146,7 @@ export function declaredPlatformFor(apex, serverIpv4) {
|
|
|
134
146
|
services[key] = {
|
|
135
147
|
enabled,
|
|
136
148
|
hostname: defaultPlatformHostname(key, apex),
|
|
137
|
-
gitRef: !enabled
|
|
149
|
+
gitRef: !enabled && !OWN_SERVICE_KEYS.has(key)
|
|
138
150
|
? ''
|
|
139
151
|
: key === 'mail'
|
|
140
152
|
? DEFAULT_MAIL_IMAGE_TAG
|
|
@@ -148,9 +160,12 @@ export function declaredPlatformFor(apex, serverIpv4) {
|
|
|
148
160
|
apex,
|
|
149
161
|
...(serverIpv4 ? { serverIpv4 } : {}),
|
|
150
162
|
...services,
|
|
163
|
+
// The platform runs no mail server, but every company's mail goes through the shared relay.
|
|
164
|
+
mail: { ...services.mail, relay: true },
|
|
151
165
|
// Off, like `sfu`: the pin is an Archon release tag and digest, which exist
|
|
152
166
|
// only after Archon runs. Declared on with no pin, it would be reported
|
|
153
167
|
// blocked and the bootstrap would never be complete.
|
|
154
168
|
uptime: { enabled: false, tag: '', sha256: '' },
|
|
169
|
+
heartbeatsProvider: 'self-hosted',
|
|
155
170
|
};
|
|
156
171
|
}
|
|
@@ -20,6 +20,24 @@ export interface PinnedVersions {
|
|
|
20
20
|
}
|
|
21
21
|
export declare function loadVersions(): PinnedVersions;
|
|
22
22
|
export declare const VERSIONS: PinnedVersions;
|
|
23
|
+
/**
|
|
24
|
+
* The multi-arch index digest of each tag in `versions.json`, in
|
|
25
|
+
* `versions.digests.json` beside it. Kept apart so every tag comparison
|
|
26
|
+
* (applied refs, version-change lines) stays on the tag. Validated on read:
|
|
27
|
+
* a malformed digest, or a key of `versions.json` without one, throws.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadDigests(): PinnedVersions;
|
|
30
|
+
export declare const DIGESTS: PinnedVersions;
|
|
31
|
+
/**
|
|
32
|
+
* `<tag>@<digest>` for an image string handed to Coolify or a Dockerfile.
|
|
33
|
+
* Only the image string uses it; the recorded applied ref stays the tag.
|
|
34
|
+
*/
|
|
35
|
+
export declare function pinnedRef(key: keyof PinnedVersions): string;
|
|
36
|
+
/**
|
|
37
|
+
* A declared tag that equals the pinned one gets its digest; any other
|
|
38
|
+
* declared tag (an override) is passed through unchanged.
|
|
39
|
+
*/
|
|
40
|
+
export declare function pinnedTagFor(key: keyof PinnedVersions, tag: string): string;
|
|
23
41
|
/**
|
|
24
42
|
* Why an image tag cannot be used, or `null`. #248: generalised out of what
|
|
25
43
|
* was `platform-providers.ts`'s `mailImageTagBlocked`, the one guard
|
|
@@ -28,3 +46,5 @@ export declare const VERSIONS: PinnedVersions;
|
|
|
28
46
|
* `"ZITADEL"`, `"LiveKit"`, ...); `example` is a real pinned tag to suggest.
|
|
29
47
|
*/
|
|
30
48
|
export declare function floatingTagBlocked(label: string, tag: string, example: string): string | null;
|
|
49
|
+
/** {@link floatingTagBlocked} for a git ref: it also refuses `main`, `master` and `HEAD`. */
|
|
50
|
+
export declare function floatingRefBlocked(label: string, ref: string, example: string): string | null;
|
|
@@ -10,6 +10,43 @@ export function loadVersions() {
|
|
|
10
10
|
return JSON.parse(readFileSync(VERSIONS_PATH, 'utf8'));
|
|
11
11
|
}
|
|
12
12
|
export const VERSIONS = loadVersions();
|
|
13
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
14
|
+
const DIGESTS_PATH = new URL('../../versions.digests.json', import.meta.url);
|
|
15
|
+
/**
|
|
16
|
+
* The multi-arch index digest of each tag in `versions.json`, in
|
|
17
|
+
* `versions.digests.json` beside it. Kept apart so every tag comparison
|
|
18
|
+
* (applied refs, version-change lines) stays on the tag. Validated on read:
|
|
19
|
+
* a malformed digest, or a key of `versions.json` without one, throws.
|
|
20
|
+
*/
|
|
21
|
+
export function loadDigests() {
|
|
22
|
+
const raw = JSON.parse(readFileSync(DIGESTS_PATH, 'utf8'));
|
|
23
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
24
|
+
throw new Error('versions.digests.json must be a JSON object');
|
|
25
|
+
}
|
|
26
|
+
const record = raw;
|
|
27
|
+
for (const key of Object.keys(loadVersions())) {
|
|
28
|
+
const value = record[key];
|
|
29
|
+
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
|
|
30
|
+
throw new Error(`versions.digests.json: "${key}" must be a sha256:<64 hex> digest`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return record;
|
|
34
|
+
}
|
|
35
|
+
export const DIGESTS = loadDigests();
|
|
36
|
+
/**
|
|
37
|
+
* `<tag>@<digest>` for an image string handed to Coolify or a Dockerfile.
|
|
38
|
+
* Only the image string uses it; the recorded applied ref stays the tag.
|
|
39
|
+
*/
|
|
40
|
+
export function pinnedRef(key) {
|
|
41
|
+
return `${VERSIONS[key]}@${DIGESTS[key]}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A declared tag that equals the pinned one gets its digest; any other
|
|
45
|
+
* declared tag (an override) is passed through unchanged.
|
|
46
|
+
*/
|
|
47
|
+
export function pinnedTagFor(key, tag) {
|
|
48
|
+
return tag === VERSIONS[key] ? pinnedRef(key) : tag;
|
|
49
|
+
}
|
|
13
50
|
/**
|
|
14
51
|
* Tag spellings that resolve to whatever the registry currently has, not a
|
|
15
52
|
* fixed image -- a re-apply of the same declaration can silently run a
|
|
@@ -41,3 +78,12 @@ export function floatingTagBlocked(label, tag, example) {
|
|
|
41
78
|
}
|
|
42
79
|
return null;
|
|
43
80
|
}
|
|
81
|
+
/** Branch names that move, refused for a git ref on top of the floating image tags. */
|
|
82
|
+
const FLOATING_GIT_REFS = new Set(['main', 'master', 'head']);
|
|
83
|
+
/** {@link floatingTagBlocked} for a git ref: it also refuses `main`, `master` and `HEAD`. */
|
|
84
|
+
export function floatingRefBlocked(label, ref, example) {
|
|
85
|
+
if (FLOATING_GIT_REFS.has(ref.trim().toLowerCase())) {
|
|
86
|
+
return `the ${label} image tag "${ref}" floats; declare a pinned release (for example ${example})`;
|
|
87
|
+
}
|
|
88
|
+
return floatingTagBlocked(label, ref, example);
|
|
89
|
+
}
|
package/dist/calls-sfu.js
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
* `calls-sfu-deploy-support.ts`'s `fileWriteMode`), never logged, scrubbed
|
|
53
53
|
* from any error `calls-sfu-deploy.mjs` prints.
|
|
54
54
|
*/
|
|
55
|
-
import { VERSIONS } from './bootstrap/versions.js';
|
|
55
|
+
import { VERSIONS, pinnedRef } from './bootstrap/versions.js';
|
|
56
56
|
import { SFU_SLOT_MAX } from './org.js';
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
// The guard
|
|
@@ -214,7 +214,7 @@ const CLOUDFLARED_METRICS_ADDRESS = '127.0.0.1:20241';
|
|
|
214
214
|
export function dockerComposeYaml() {
|
|
215
215
|
return `services:
|
|
216
216
|
livekit:
|
|
217
|
-
image:
|
|
217
|
+
image: livekit/livekit-server:${pinnedRef('livekit')}
|
|
218
218
|
restart: unless-stopped
|
|
219
219
|
network_mode: host
|
|
220
220
|
volumes:
|
|
@@ -227,7 +227,7 @@ export function dockerComposeYaml() {
|
|
|
227
227
|
retries: 3
|
|
228
228
|
start_period: 30s
|
|
229
229
|
cloudflared:
|
|
230
|
-
image:
|
|
230
|
+
image: cloudflare/cloudflared:${pinnedRef('cloudflared')}
|
|
231
231
|
restart: unless-stopped
|
|
232
232
|
network_mode: host
|
|
233
233
|
env_file:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceSchema, ServicesConfig, } from './org.js';
|
|
2
|
-
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, appEntry, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateSite, } from './org.js';
|
|
3
|
-
export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, } from './platform.js';
|
|
4
|
-
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
1
|
+
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceSchema, ServicesConfig, VersionPin, VersionPinPart, } from './org.js';
|
|
2
|
+
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, VERSION_PIN_PARTS, appEntry, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateVersionPin, validateSite, } from './org.js';
|
|
3
|
+
export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, HeartbeatsProvider, } from './platform.js';
|
|
4
|
+
export { PLATFORM_SERVICE_KEYS, HEARTBEATS_PROVIDERS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
5
5
|
import { type PinnedVersions } from './bootstrap/versions.js';
|
|
6
6
|
export type { PinnedVersions };
|
|
7
7
|
export declare const PINNED_VERSIONS: Readonly<PinnedVersions>;
|
|
@@ -19,7 +19,10 @@ export { createPostgresAuditLog } from './store/audit.js';
|
|
|
19
19
|
export type { FoundryStoreDb } from './store/postgres.js';
|
|
20
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
21
21
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, OrgPostgresPlan, PlatformPlanDetail, MailRoute, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, UnconfirmedUpgrade, } from './plan/index.js';
|
|
22
|
+
export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
|
|
22
23
|
export { AdoptRefusedError } from './plan/index.js';
|
|
24
|
+
export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
|
|
25
|
+
export type { BackupGatePorts } from './plan/index.js';
|
|
23
26
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
24
27
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, DnsPort, EdgePlatformAccess, EdgePlatformDns, EdgePlatformRateLimits, EdgePlatformWorkers, EdgePlatformZones, EdgeZonesOptions, ZonePort, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, EdgeWorkersOptions, EdgePlatformTunnels, TunnelPort, } from './plan/index.js';
|
|
25
28
|
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
@@ -35,6 +38,8 @@ export type { PlanOwnSfuOptions, SfuAppDeps, SfuAppInput } from './plan/index.js
|
|
|
35
38
|
export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuPortsMappings, } from './plan/index.js';
|
|
36
39
|
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, LoginHostDiscoveryPort, } from './plan/index.js';
|
|
37
40
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
41
|
+
export { NO_DOMAIN_ZONE, ownHostnames } from './plan/index.js';
|
|
42
|
+
export type { OwnHostnames } from './plan/index.js';
|
|
38
43
|
export type { EnsureSignInOrgOptions } from './plan/index.js';
|
|
39
44
|
export { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ensureSignInOrg, registerSignInDomain, signInOrgDomain, } from './plan/index.js';
|
|
40
45
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, appEntry, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateSite, } from './org.js';
|
|
2
|
-
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
1
|
+
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, VERSION_PIN_PARTS, appEntry, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateVersionPin, validateSite, } from './org.js';
|
|
2
|
+
export { PLATFORM_SERVICE_KEYS, HEARTBEATS_PROVIDERS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
3
3
|
// archon#273: the pinned platform service versions the planner deploys, so a
|
|
4
4
|
// host can show declared against running. Readonly: it is the planner's own
|
|
5
5
|
// object, not a copy.
|
|
@@ -11,7 +11,9 @@ export { StaleLeaseError } from './store.js';
|
|
|
11
11
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
12
12
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
13
13
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
14
|
+
export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
|
|
14
15
|
export { AdoptRefusedError } from './plan/index.js';
|
|
16
|
+
export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
|
|
15
17
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
16
18
|
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
17
19
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
@@ -20,6 +22,7 @@ export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICE
|
|
|
20
22
|
export { OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY, OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, provisionFilesHost, provisionJobsHost, provisionMailServer, } from './plan/index.js';
|
|
21
23
|
export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuPortsMappings, } from './plan/index.js';
|
|
22
24
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
25
|
+
export { NO_DOMAIN_ZONE, ownHostnames } from './plan/index.js';
|
|
23
26
|
export { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ensureSignInOrg, registerSignInDomain, signInOrgDomain, } from './plan/index.js';
|
|
24
27
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
25
28
|
export { healthTargets, platformHealthTargets } from './plan/index.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- @wtfalch/foundry: foundry#544 -- which Healthchecks instance manages the
|
|
2
|
+
-- platform's heartbeat checks, decoupled from whether foundry also deploys
|
|
3
|
+
-- its own self-hosted server (`hc_enabled`, foundry#540). 'self-hosted' is
|
|
4
|
+
-- the default and today's only behaviour; 'hosted' needs no self-hosted
|
|
5
|
+
-- server applied at all. Mirrors `src/store/schema.ts`'s
|
|
6
|
+
-- `foundryDeclaredPlatform` table addition.
|
|
7
|
+
|
|
8
|
+
alter table foundry_declared_platform
|
|
9
|
+
add column if not exists heartbeats_provider text not null default 'self-hosted';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- @wtfalch/foundry: a company's version hold. `version_pin` is a json object of
|
|
2
|
+
-- part to git ref or image tag (auth, mail, files, jobs, sfu, calls); null
|
|
3
|
+
-- follows the platform. Additive. Mirrors src/store/schema.ts's `declaredOrgs`.
|
|
4
|
+
|
|
5
|
+
alter table foundry_declared_orgs
|
|
6
|
+
add column if not exists version_pin jsonb;
|
package/dist/org.d.ts
CHANGED
|
@@ -316,7 +316,14 @@ export interface HostingEntry {
|
|
|
316
316
|
hostnames: string[];
|
|
317
317
|
/** A Postgres database beside the app. At most one entry per org. */
|
|
318
318
|
database: boolean;
|
|
319
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* The Linux capabilities of the entry's app container. Absent or `'restricted'`: all dropped, then only the
|
|
321
|
+
* few a web server or node entrypoint needs added back. `'standard'`: Docker's defaults minus raw sockets,
|
|
322
|
+
* device nodes, file capabilities, chroot and audit writes. A fallback for an image that will not start
|
|
323
|
+
* restricted.
|
|
324
|
+
*/
|
|
325
|
+
capabilities?: 'restricted' | 'standard';
|
|
326
|
+
/** Deploy on every push to `branch`. A host should declare `false`; foundry creates every other app with auto-deploy off. */
|
|
320
327
|
autoDeploy: boolean;
|
|
321
328
|
/**
|
|
322
329
|
* Cloudflare Access gates on this entry's own `hostnames`. Empty means no
|
|
@@ -486,13 +493,11 @@ export declare function appEntry(hosting: HostingConfig): HostingEntry | undefin
|
|
|
486
493
|
* sends from. Each is read from `StepExecutionContext.recordedIds()` in
|
|
487
494
|
* `executor.ts`'s `pushAppEnv`, never minted again there.
|
|
488
495
|
*/
|
|
489
|
-
export type AppEnvSource = 'filesOrganisationId' | 'aiOrganisationId' | 'mailDomain' | 'sharedSecret'
|
|
496
|
+
export type AppEnvSource = 'filesOrganisationId' | 'aiOrganisationId' | 'mailDomain' | 'sharedSecret';
|
|
490
497
|
/**
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
* `LETTERMINT_API_TOKEN`). Any hosting entry of any customer org may name it
|
|
495
|
-
* (William, 2026-10-04): mail is Lettermint, and the key is the platform's.
|
|
498
|
+
* `sharedSecret` is the one secret source: {@link EnvSharedSecret}. The
|
|
499
|
+
* platform's Lettermint key is not a source any more (2026-10-11): each
|
|
500
|
+
* company uses its own mail key.
|
|
496
501
|
*/
|
|
497
502
|
export declare const APP_ENV_SOURCES: ReadonlySet<AppEnvSource>;
|
|
498
503
|
/** The sources whose value is a secret: resolved at apply time, never recorded. */
|
|
@@ -516,6 +521,29 @@ export declare function validateHosting(hosting: HostingConfig, names?: {
|
|
|
516
521
|
locked: ReadonlySet<string>;
|
|
517
522
|
}): string | null;
|
|
518
523
|
export declare function validateDomain(domain: string): string | null;
|
|
524
|
+
/**
|
|
525
|
+
* A broker entry name foundry writes itself: Archon's, and any company's Boule.
|
|
526
|
+
* A hosting entry may not declare that it replaces one.
|
|
527
|
+
*/
|
|
528
|
+
export declare function isFoundryOwnedBrokerEntryName(name: string): boolean;
|
|
529
|
+
/**
|
|
530
|
+
* `null` when `domain` may be claimed by company `orgId`, given the platform's
|
|
531
|
+
* apex and owner company and every other company's declared domain; otherwise
|
|
532
|
+
* the reason it may not. The platform's apex and anything under it belong to
|
|
533
|
+
* the owner company alone, and no company takes a parent or subdomain of
|
|
534
|
+
* another's, since Cloudflare would resolve both to one zone.
|
|
535
|
+
*/
|
|
536
|
+
export declare function validateDomainClaim(domain: string, claim: {
|
|
537
|
+
orgId: string;
|
|
538
|
+
platform: {
|
|
539
|
+
apex: string;
|
|
540
|
+
ownerOrgId: string;
|
|
541
|
+
} | null;
|
|
542
|
+
others: readonly {
|
|
543
|
+
id: string;
|
|
544
|
+
domain: string | null;
|
|
545
|
+
}[];
|
|
546
|
+
}): string | null;
|
|
519
547
|
/**
|
|
520
548
|
* `null` when `site` is valid; otherwise the reason it is not (#101).
|
|
521
549
|
*
|
|
@@ -526,11 +554,9 @@ export declare function validateDomain(domain: string): string | null;
|
|
|
526
554
|
* and cms there (`site`), never both -- both would otherwise fight over the
|
|
527
555
|
* same hostname (https://github.com/wtfalch/foundry/issues/99).
|
|
528
556
|
*
|
|
529
|
-
* - `site.enabled`
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* and the issuer, the site has no meaning anywhere but the org's own
|
|
533
|
-
* domain, so turning it on always means the domain opt-in too.
|
|
557
|
+
* - `site.enabled` with a `domain` requires `site.usesDomain`: the site then
|
|
558
|
+
* lives on the domain. Without a `domain` it lives on `<slug>.oikos.host`
|
|
559
|
+
* and `site.usesDomain` must be off.
|
|
534
560
|
* - `site.enabled` and `hosting.enabled` cannot both be on.
|
|
535
561
|
* - `site.enabled` refuses an `issuerApps` entry named `cms`: the fixed cms
|
|
536
562
|
* client (#139, `plan/diff.ts`'s `planCmsClient`) already owns the
|
|
@@ -548,7 +574,25 @@ export declare function validateSite(org: DeclaredOrg): string | null;
|
|
|
548
574
|
* reason to pin at all.
|
|
549
575
|
*/
|
|
550
576
|
export declare function validateCalls(org: DeclaredOrg): string | null;
|
|
551
|
-
/**
|
|
577
|
+
/** The parts of a company that can be held on their own version. */
|
|
578
|
+
export declare const VERSION_PIN_PARTS: readonly ["auth", "mail", "files", "jobs", "sfu", "calls"];
|
|
579
|
+
export type VersionPinPart = (typeof VERSION_PIN_PARTS)[number];
|
|
580
|
+
/**
|
|
581
|
+
* A part held at a git ref (auth, files, jobs) or an image tag (mail, sfu, calls). Absent part: the platform's value.
|
|
582
|
+
* `calls` is the Synapse tag of the calls stack. It is used when the stack is created; a running stack is not moved
|
|
583
|
+
* (nothing patches the image tag of an existing app, see the Synapse create in `plan/executor.ts`).
|
|
584
|
+
*/
|
|
585
|
+
export type VersionPin = Partial<Record<VersionPinPart, string>>;
|
|
586
|
+
/**
|
|
587
|
+
* `versionPin` holds parts of a company on an older release while others move.
|
|
588
|
+
* Every value is a fixed ref or tag: a floating one would defeat the hold.
|
|
589
|
+
* Also run on a stored value when it is read, so a bad row is not trusted.
|
|
590
|
+
*/
|
|
591
|
+
export declare function validateVersionPin(org: Pick<DeclaredOrg, 'versionPin'>): string | null;
|
|
592
|
+
/**
|
|
593
|
+
* Every company runs its own sign-in, mail, files, jobs and calls SFU in its own containers, the platform's owner company too.
|
|
594
|
+
* A company without a domain runs them on oikos.host.
|
|
595
|
+
*/
|
|
552
596
|
export interface ServicesConfig {
|
|
553
597
|
/** true: own copies of the services. false (default): the platform's shared ones. */
|
|
554
598
|
own: boolean;
|
|
@@ -560,7 +604,7 @@ export interface ServicesConfig {
|
|
|
560
604
|
sfuSlot?: number | null;
|
|
561
605
|
}
|
|
562
606
|
export declare const SFU_SLOT_MAX = 20;
|
|
563
|
-
/** The only reader of `services.own
|
|
607
|
+
/** The only reader of `services.own`. Every company runs its own services, the platform's owner company too; a company without a domain runs them on oikos.host. */
|
|
564
608
|
export declare function ownsServices(org: Pick<DeclaredOrg, 'services'>): boolean;
|
|
565
609
|
/** The pure part of the `services` rules; that no two orgs share an `sfuSlot` is the store's check. */
|
|
566
610
|
export declare function validateServices(org: DeclaredOrg): string | null;
|
|
@@ -840,6 +884,8 @@ export interface DeclaredOrg {
|
|
|
840
884
|
hosting: HostingConfig;
|
|
841
885
|
/** Absent means `{ own: false }`. Optional so every existing literal and test fixture compiles unchanged, as `calls.versionPin` and `MailConfig.mailApps` do. */
|
|
842
886
|
services?: ServicesConfig;
|
|
887
|
+
/** Parts of this company held on their own version. Absent or null follows the platform. Ignored for the owner company. */
|
|
888
|
+
versionPin?: VersionPin | null;
|
|
843
889
|
sso: SsoConfig;
|
|
844
890
|
guests: GuestsConfig;
|
|
845
891
|
solo: SoloConfig;
|