@wtfalch/foundry 0.76.1 → 0.77.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/app-canary-worker.d.ts +35 -0
- package/dist/app-canary-worker.js +56 -0
- package/dist/audit/connector.d.ts +6 -2
- package/dist/audit/connector.js +3 -2
- package/dist/audit/evidence.d.ts +4 -0
- package/dist/audit/evidence.js +14 -2
- package/dist/audit/host-store.d.ts +58 -0
- package/dist/audit/host-store.js +147 -0
- package/dist/audit/host.d.ts +53 -0
- package/dist/audit/host.js +294 -0
- package/dist/audit/index.d.ts +2 -0
- package/dist/audit/index.js +2 -0
- package/dist/audit/registry.d.ts +14 -0
- package/dist/audit/sealer-role.d.ts +12 -0
- package/dist/audit/sealer-role.js +26 -2
- package/dist/audit/tick.d.ts +10 -2
- package/dist/audit/tick.js +15 -2
- package/dist/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/entry-rules.d.ts +35 -0
- package/dist/entry-rules.js +35 -0
- package/dist/hosted-label.js +2 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -1
- package/dist/migrations/0072_audit_host.sql +46 -0
- package/dist/migrations/0074_platform_app_canary.sql +11 -0
- package/dist/modules/ai.js +15 -1
- package/dist/modules/assistants.js +3 -0
- package/dist/modules/email.d.ts +6 -1
- package/dist/modules/email.js +88 -5
- package/dist/modules/files.js +79 -2
- package/dist/modules/index.js +7 -0
- package/dist/modules/jobs.d.ts +5 -1
- package/dist/modules/jobs.js +74 -4
- package/dist/modules/reporting.d.ts +9 -0
- package/dist/modules/reporting.js +29 -0
- package/dist/modules/sign-in.js +20 -2
- package/dist/modules/types.d.ts +2 -0
- package/dist/modules/workspaces.js +3 -0
- package/dist/org.d.ts +114 -13
- package/dist/org.js +233 -20
- package/dist/plan/ai-boule-key-apply.d.ts +65 -0
- package/dist/plan/ai-boule-key-apply.js +169 -0
- package/dist/plan/ai-boule-key.d.ts +89 -0
- package/dist/plan/ai-boule-key.js +195 -0
- package/dist/plan/analytics.d.ts +8 -1
- package/dist/plan/analytics.js +8 -1
- package/dist/plan/app-canary-apply.d.ts +49 -0
- package/dist/plan/app-canary-apply.js +232 -0
- package/dist/plan/app-canary.d.ts +44 -0
- package/dist/plan/app-canary.js +110 -0
- package/dist/plan/blue-green-apply.d.ts +6 -2
- package/dist/plan/blue-green-apply.js +14 -3
- package/dist/plan/broker-entries.js +20 -5
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +22 -0
- package/dist/plan/diff.js +231 -18
- package/dist/plan/entry-files-key.d.ts +39 -3
- package/dist/plan/entry-files-key.js +103 -6
- package/dist/plan/entry-jobs-boule-key-apply.d.ts +65 -0
- package/dist/plan/entry-jobs-boule-key-apply.js +192 -0
- package/dist/plan/entry-jobs-boule-key.d.ts +59 -0
- package/dist/plan/entry-jobs-boule-key.js +141 -0
- package/dist/plan/entry-jobs.d.ts +2 -0
- package/dist/plan/entry-reporting-key.d.ts +36 -0
- package/dist/plan/entry-reporting-key.js +136 -0
- package/dist/plan/executor.d.ts +108 -1
- package/dist/plan/executor.js +669 -23
- package/dist/plan/files-boule-key-apply.d.ts +73 -0
- package/dist/plan/files-boule-key-apply.js +223 -0
- package/dist/plan/files-boule-key.d.ts +92 -0
- package/dist/plan/files-boule-key.js +175 -0
- package/dist/plan/hostnames.js +4 -0
- package/dist/plan/index.d.ts +4 -0
- package/dist/plan/index.js +2 -0
- package/dist/plan/jobs-scheduler-http.d.ts +13 -0
- package/dist/plan/jobs-scheduler-http.js +39 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +1 -1
- package/dist/plan/locked-hostnames.js +4 -1
- package/dist/plan/login-host.js +2 -2
- package/dist/plan/org-names.js +3 -0
- package/dist/plan/own-diff.js +3 -2
- package/dist/plan/own-jobs-route.d.ts +5 -2
- package/dist/plan/own-jobs-route.js +6 -2
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +24 -0
- package/dist/plan/plan-apply.js +39 -4
- package/dist/plan/platform-diff.d.ts +2 -0
- package/dist/plan/platform-diff.js +15 -1
- package/dist/plan/platform-providers.d.ts +9 -0
- package/dist/plan/platform-providers.js +24 -0
- package/dist/plan/providers-readers.js +25 -1
- package/dist/plan/registered-app-env.d.ts +31 -0
- package/dist/plan/registered-app-env.js +63 -0
- package/dist/plan/registered-app.d.ts +121 -0
- package/dist/plan/registered-app.js +262 -0
- package/dist/plan/release-host.js +2 -1
- package/dist/plan/reporting-boule.d.ts +18 -0
- package/dist/plan/reporting-boule.js +44 -0
- package/dist/plan/reporting.d.ts +90 -0
- package/dist/plan/reporting.js +135 -0
- package/dist/plan/stage-env.js +1 -0
- package/dist/plan/stage-isolation.d.ts +3 -1
- package/dist/plan/stage-isolation.js +20 -2
- package/dist/plan/stage-services.d.ts +3 -1
- package/dist/plan/stage-services.js +8 -1
- package/dist/plan/stage-sink.d.ts +2 -2
- package/dist/plan/stage-sink.js +2 -2
- package/dist/plan/workspace-client.d.ts +1 -1
- package/dist/plan/workspace-client.js +2 -1
- package/dist/plan/workspace-sign-in.d.ts +1 -1
- package/dist/plan/workspace-sign-in.js +39 -7
- package/dist/platform.d.ts +25 -0
- package/dist/platform.js +20 -0
- package/dist/service-labels.d.ts +8 -0
- package/dist/service-labels.js +8 -0
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +5 -2
- package/dist/store/postgres.js +13 -0
- package/dist/store/schema.d.ts +485 -2
- package/dist/store/schema.js +38 -1
- package/dist/worker-app-config.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { APPS_DOMAIN } from '../org.js';
|
|
2
|
+
import { appCanaryArtifactUrl } from '../platform.js';
|
|
3
|
+
import { WORKER_APP_DNS_RECORD, withCallTimeout } from '../worker-app-upload.js';
|
|
4
|
+
import { PLATFORM_APP_CANARY_APPLIED_SHA256_SCOPE_KEY, PLATFORM_APP_CANARY_APPLIED_TAG_SCOPE_KEY, PLATFORM_APP_CANARY_DNS_SCOPE_KEY, PLATFORM_APP_CANARY_HOSTNAME, PLATFORM_APP_CANARY_HOSTNAME_SCOPE_KEY, PLATFORM_APP_CANARY_REMOVED_SCOPE_KEY, PLATFORM_APP_CANARY_RESOURCE_KEY, PLATFORM_APP_CANARY_ROUTE_SCOPE_KEY, PLATFORM_APP_CANARY_SCRIPT_NAME, appCanaryDeployed, appCanaryRecorded, } from './app-canary.js';
|
|
5
|
+
import { PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY } from './platform-diff.js';
|
|
6
|
+
/** The date Cloudflare's Workers runtime compatibility is pinned to; the canary needs nothing later. */
|
|
7
|
+
const APP_CANARY_COMPATIBILITY_DATE = '2026-10-01';
|
|
8
|
+
/** One outside call's limit: 60 s. */
|
|
9
|
+
const APP_CANARY_CALL_TIMEOUT_MS = 60_000;
|
|
10
|
+
/** How many times `GET https://<host>/` is asked before the step fails (rule 5: never forever). */
|
|
11
|
+
const APP_CANARY_PROBE_ATTEMPTS = 6;
|
|
12
|
+
/** Waits between probes: the route and record can take seconds to answer. */
|
|
13
|
+
const APP_CANARY_PROBE_INTERVAL_MS = 5_000;
|
|
14
|
+
/** One probe's limit. */
|
|
15
|
+
const APP_CANARY_PROBE_TIMEOUT_MS = 10_000;
|
|
16
|
+
const AT = 'platform:app-canary';
|
|
17
|
+
const PATTERN = `${PLATFORM_APP_CANARY_HOSTNAME}/*`;
|
|
18
|
+
function parse(raw) {
|
|
19
|
+
if (!raw)
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const isOurRecord = (r) => r.type === WORKER_APP_DNS_RECORD.type && r.content === WORKER_APP_DNS_RECORD.content;
|
|
29
|
+
async function defaultProbe(url) {
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
redirect: 'manual',
|
|
33
|
+
signal: AbortSignal.timeout(APP_CANARY_PROBE_TIMEOUT_MS),
|
|
34
|
+
});
|
|
35
|
+
await response.body?.cancel();
|
|
36
|
+
return response.status;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Why the canary cannot be planned yet, read live (rule 2), or `undefined`.
|
|
40
|
+
* A script, route or DNS record on the canary's name that foundry did not
|
|
41
|
+
* make is never taken over: the plan says so and plans nothing, rather than a
|
|
42
|
+
* step that throws on every apply. A missing apps zone is the same.
|
|
43
|
+
*/
|
|
44
|
+
export async function appCanaryBlockedReason(deps, platform, recordedIds) {
|
|
45
|
+
const enabled = platform.appCanary?.enabled === true;
|
|
46
|
+
if (!enabled && !appCanaryRecorded(recordedIds))
|
|
47
|
+
return undefined;
|
|
48
|
+
if (!deps) {
|
|
49
|
+
return 'the app-layer canary is blocked, not planned: the host built the platform executor without its app-canary dependencies';
|
|
50
|
+
}
|
|
51
|
+
const edgeUrl = recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
52
|
+
if (!enabled || !edgeUrl || !platform.appCanary?.tag)
|
|
53
|
+
return undefined;
|
|
54
|
+
const ms = deps.callTimeoutMs ?? APP_CANARY_CALL_TIMEOUT_MS;
|
|
55
|
+
const call = (label, task) => withCallTimeout(ms, `${AT}: ${label}`, task);
|
|
56
|
+
try {
|
|
57
|
+
const zones = await deps.zones(edgeUrl);
|
|
58
|
+
const zone = await call('find the apps zone', () => zones.findZoneByApex(APPS_DOMAIN));
|
|
59
|
+
if (!zone) {
|
|
60
|
+
return `the app-layer canary is blocked, not planned: edge has no zone for ${APPS_DOMAIN}`;
|
|
61
|
+
}
|
|
62
|
+
const workers = await deps.workers(edgeUrl);
|
|
63
|
+
const route = await call('find the route', () => workers.findRoute(zone.id, PATTERN));
|
|
64
|
+
if (route && route.script !== PLATFORM_APP_CANARY_SCRIPT_NAME) {
|
|
65
|
+
return `the app-layer canary is blocked, not planned: the route ${PATTERN} runs another Worker; remove it by hand`;
|
|
66
|
+
}
|
|
67
|
+
if (!appCanaryDeployed(recordedIds)) {
|
|
68
|
+
if (await call('check the script name', () => workers.scriptExists(PLATFORM_APP_CANARY_SCRIPT_NAME))) {
|
|
69
|
+
return `the app-layer canary is blocked, not planned: a Worker named ${PLATFORM_APP_CANARY_SCRIPT_NAME} already exists at Cloudflare and foundry did not make it; remove it by hand`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!recordedIds[PLATFORM_APP_CANARY_DNS_SCOPE_KEY]) {
|
|
73
|
+
const dns = await deps.dns(edgeUrl);
|
|
74
|
+
const found = await call('look for the DNS record', () => dns.findDnsRecord(zone.name, PLATFORM_APP_CANARY_HOSTNAME));
|
|
75
|
+
if (found.length > 0 && !found.some(isOurRecord)) {
|
|
76
|
+
return `the app-layer canary is blocked, not planned: ${PLATFORM_APP_CANARY_HOSTNAME} already has a DNS record foundry did not make; remove it by hand`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return 'the app-layer canary is blocked, not planned: edge could not be asked what is on the canary name';
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* foundry#1133: deploys the canary on the managed-app layer and records the
|
|
87
|
+
* hostname Archon's uptime probes, last, once it answers 200. The script is
|
|
88
|
+
* recorded right after its upload (a later failure leaves a half-made canary
|
|
89
|
+
* foundry owns, which the next apply repairs); the record and the route are
|
|
90
|
+
* recorded as each is made. Disabled, it clears the hostname first, so Archon
|
|
91
|
+
* stops probing, then removes the route, the record and the script.
|
|
92
|
+
*/
|
|
93
|
+
export async function provisionPlatformAppCanary(context, deps) {
|
|
94
|
+
const recorded = context.recordedIds();
|
|
95
|
+
const edgeUrl = recorded[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
96
|
+
if (!edgeUrl) {
|
|
97
|
+
throw new Error(`${AT}: edge has not been applied yet (no recorded ${PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY}); the canary is deployed through it`);
|
|
98
|
+
}
|
|
99
|
+
const ms = deps.callTimeoutMs ?? APP_CANARY_CALL_TIMEOUT_MS;
|
|
100
|
+
const call = (label, task) => withCallTimeout(ms, `${AT}: ${label}`, task);
|
|
101
|
+
const workers = await deps.workers(edgeUrl);
|
|
102
|
+
const zones = await deps.zones(edgeUrl);
|
|
103
|
+
const dns = await deps.dns(edgeUrl);
|
|
104
|
+
const canary = context.platform.appCanary;
|
|
105
|
+
if (!canary?.enabled) {
|
|
106
|
+
await removeAppCanary(context, { workers, zones, dns, call });
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
if (!canary.tag || !canary.sha256) {
|
|
110
|
+
throw new Error(`${AT}: no artifact is pinned (appCanary.tag, appCanary.sha256)`);
|
|
111
|
+
}
|
|
112
|
+
// The zone and the route first: a foreign route is refused before anything is uploaded.
|
|
113
|
+
const zone = await call('find the apps zone', () => zones.findZoneByApex(APPS_DOMAIN));
|
|
114
|
+
if (!zone)
|
|
115
|
+
throw new Error(`${AT}: edge has no zone for ${APPS_DOMAIN}`);
|
|
116
|
+
const existing = await call('find the route', () => workers.findRoute(zone.id, PATTERN));
|
|
117
|
+
if (existing && existing.script !== PLATFORM_APP_CANARY_SCRIPT_NAME) {
|
|
118
|
+
throw new Error(`${AT}: the route ${PATTERN} runs another Worker; foundry does not take it over`);
|
|
119
|
+
}
|
|
120
|
+
// ---- the script ----------------------------------------------------------
|
|
121
|
+
await call('upload the script', () => workers.uploadScript({
|
|
122
|
+
name: PLATFORM_APP_CANARY_SCRIPT_NAME,
|
|
123
|
+
artifactUrl: appCanaryArtifactUrl(canary.tag),
|
|
124
|
+
artifactSha256: canary.sha256,
|
|
125
|
+
compatibilityDate: APP_CANARY_COMPATIBILITY_DATE,
|
|
126
|
+
bindings: [],
|
|
127
|
+
}));
|
|
128
|
+
context.recordScopeId(PLATFORM_APP_CANARY_RESOURCE_KEY, PLATFORM_APP_CANARY_SCRIPT_NAME);
|
|
129
|
+
context.recordScopeId(PLATFORM_APP_CANARY_REMOVED_SCOPE_KEY, '');
|
|
130
|
+
await context.persist?.();
|
|
131
|
+
// ---- the host's record ---------------------------------------------------
|
|
132
|
+
// `companyincluded.app` has no wildcard, and edge writes a company route's
|
|
133
|
+
// record itself but not a platform route's, so the host would not resolve.
|
|
134
|
+
const recordedDns = parse(context.recordedIds()[PLATFORM_APP_CANARY_DNS_SCOPE_KEY]);
|
|
135
|
+
if (recordedDns?.zone !== zone.name) {
|
|
136
|
+
const found = await call('look for the DNS record', () => dns.findDnsRecord(zone.name, PLATFORM_APP_CANARY_HOSTNAME));
|
|
137
|
+
const ours = found.find(isOurRecord);
|
|
138
|
+
let id;
|
|
139
|
+
if (ours)
|
|
140
|
+
id = ours.id;
|
|
141
|
+
else if (found.length > 0) {
|
|
142
|
+
throw new Error(`${AT}: ${PLATFORM_APP_CANARY_HOSTNAME} already has a DNS record foundry did not make; it does not replace it`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
id = (await call('create the DNS record', () => dns.createDnsRecord(zone.name, {
|
|
146
|
+
...WORKER_APP_DNS_RECORD,
|
|
147
|
+
name: PLATFORM_APP_CANARY_HOSTNAME,
|
|
148
|
+
proxied: true,
|
|
149
|
+
ttl: 1,
|
|
150
|
+
}))).id;
|
|
151
|
+
}
|
|
152
|
+
context.recordScopeId(PLATFORM_APP_CANARY_DNS_SCOPE_KEY, JSON.stringify({ zone: zone.name, id }));
|
|
153
|
+
await context.persist?.();
|
|
154
|
+
}
|
|
155
|
+
// ---- the route -----------------------------------------------------------
|
|
156
|
+
const routeId = existing?.id ??
|
|
157
|
+
(await call('create the route', () => workers.createRoute(zone.id, PATTERN, PLATFORM_APP_CANARY_SCRIPT_NAME))).id;
|
|
158
|
+
context.recordScopeId(PLATFORM_APP_CANARY_ROUTE_SCOPE_KEY, JSON.stringify({ zoneId: zone.id, id: routeId }));
|
|
159
|
+
await context.persist?.();
|
|
160
|
+
// ---- it answers ----------------------------------------------------------
|
|
161
|
+
await waitForAnswer(deps);
|
|
162
|
+
// Last: Archon probes the hostname only once it is here.
|
|
163
|
+
context.recordScopeId(PLATFORM_APP_CANARY_APPLIED_TAG_SCOPE_KEY, canary.tag);
|
|
164
|
+
context.recordScopeId(PLATFORM_APP_CANARY_APPLIED_SHA256_SCOPE_KEY, canary.sha256);
|
|
165
|
+
context.recordScopeId(PLATFORM_APP_CANARY_HOSTNAME_SCOPE_KEY, PLATFORM_APP_CANARY_HOSTNAME);
|
|
166
|
+
return { resourceId: PLATFORM_APP_CANARY_SCRIPT_NAME };
|
|
167
|
+
}
|
|
168
|
+
/** `GET https://<host>/` until it answers 200, a bounded number of times; the error is fixed text. */
|
|
169
|
+
async function waitForAnswer(deps) {
|
|
170
|
+
const probe = deps.probe ?? defaultProbe;
|
|
171
|
+
const attempts = deps.probeAttempts ?? APP_CANARY_PROBE_ATTEMPTS;
|
|
172
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
173
|
+
let last = 'no answer';
|
|
174
|
+
for (let attempt = 1;; attempt += 1) {
|
|
175
|
+
try {
|
|
176
|
+
const status = await probe(`https://${PLATFORM_APP_CANARY_HOSTNAME}/`);
|
|
177
|
+
if (status === 200)
|
|
178
|
+
return;
|
|
179
|
+
last = `status ${status}`;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
last = 'no answer';
|
|
183
|
+
}
|
|
184
|
+
if (attempt >= attempts) {
|
|
185
|
+
throw new Error(`${AT}: https://${PLATFORM_APP_CANARY_HOSTNAME}/ did not answer 200 after ${attempts} tries (${last}); the hostname is not recorded, so the next apply tries again`);
|
|
186
|
+
}
|
|
187
|
+
await sleep(APP_CANARY_PROBE_INTERVAL_MS);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function removeAppCanary(context, ports) {
|
|
191
|
+
const { workers, zones, dns, call } = ports;
|
|
192
|
+
// First, so Archon stops probing a canary that is about to go.
|
|
193
|
+
context.recordScopeId(PLATFORM_APP_CANARY_HOSTNAME_SCOPE_KEY, '');
|
|
194
|
+
context.recordScopeId(PLATFORM_APP_CANARY_APPLIED_TAG_SCOPE_KEY, '');
|
|
195
|
+
context.recordScopeId(PLATFORM_APP_CANARY_APPLIED_SHA256_SCOPE_KEY, '');
|
|
196
|
+
await context.persist?.();
|
|
197
|
+
const recorded = context.recordedIds();
|
|
198
|
+
const route = parse(recorded[PLATFORM_APP_CANARY_ROUTE_SCOPE_KEY]);
|
|
199
|
+
const dnsRecord = parse(recorded[PLATFORM_APP_CANARY_DNS_SCOPE_KEY]);
|
|
200
|
+
// A route made and not yet recorded is found on its pattern, and only when it runs the canary.
|
|
201
|
+
let zone;
|
|
202
|
+
const apexZone = async () => {
|
|
203
|
+
zone ??= await call('find the apps zone', () => zones.findZoneByApex(APPS_DOMAIN));
|
|
204
|
+
return zone;
|
|
205
|
+
};
|
|
206
|
+
const routes = route ? [route] : [];
|
|
207
|
+
if (!route) {
|
|
208
|
+
const z = await apexZone();
|
|
209
|
+
const found = z
|
|
210
|
+
? await call('find the route', () => workers.findRoute(z.id, PATTERN))
|
|
211
|
+
: undefined;
|
|
212
|
+
if (z && found?.script === PLATFORM_APP_CANARY_SCRIPT_NAME)
|
|
213
|
+
routes.push({ zoneId: z.id, id: found.id });
|
|
214
|
+
}
|
|
215
|
+
for (const r of routes) {
|
|
216
|
+
await call('delete the route', () => workers.deleteRoute(r.zoneId, r.id));
|
|
217
|
+
}
|
|
218
|
+
context.recordScopeId(PLATFORM_APP_CANARY_ROUTE_SCOPE_KEY, '');
|
|
219
|
+
await context.persist?.();
|
|
220
|
+
// Only the record foundry made: `AAAA 100::` on the canary's host.
|
|
221
|
+
const zoneName = dnsRecord?.zone ?? (await apexZone())?.name;
|
|
222
|
+
if (zoneName) {
|
|
223
|
+
const found = await call('look for the DNS record', () => dns.findDnsRecord(zoneName, PLATFORM_APP_CANARY_HOSTNAME));
|
|
224
|
+
for (const r of found.filter(isOurRecord)) {
|
|
225
|
+
await call('delete the DNS record', () => dns.deleteDnsRecord(zoneName, r.id));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
context.recordScopeId(PLATFORM_APP_CANARY_DNS_SCOPE_KEY, '');
|
|
229
|
+
await context.persist?.();
|
|
230
|
+
await call('delete the script', () => workers.deleteScript(PLATFORM_APP_CANARY_SCRIPT_NAME));
|
|
231
|
+
context.recordScopeId(PLATFORM_APP_CANARY_REMOVED_SCOPE_KEY, '1');
|
|
232
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { DeclaredPlatform } from '../platform.js';
|
|
2
|
+
import type { DraftStep } from './diff.js';
|
|
3
|
+
/**
|
|
4
|
+
* foundry#1133: the app-layer canary, one `cloudflare:worker` step keyed
|
|
5
|
+
* {@link PLATFORM_APP_CANARY_KEY}. `runPlatformSteps` records the script name
|
|
6
|
+
* under `cloudflare:worker:platform:app-canary` once it succeeds.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PLATFORM_APP_CANARY_KEY = "platform:app-canary";
|
|
9
|
+
/** The Worker script's fixed name at Cloudflare, on the platform's own edge organisation. */
|
|
10
|
+
export declare const PLATFORM_APP_CANARY_SCRIPT_NAME = "uptime-canary";
|
|
11
|
+
/** The canary's first label under {@link APPS_DOMAIN}; locked for companies (`PLATFORM_APP_LABELS`). */
|
|
12
|
+
export declare const PLATFORM_APP_CANARY_LABEL = "canary";
|
|
13
|
+
/** Where the canary answers: one label under the apps zone, as archon#812 requires. */
|
|
14
|
+
export declare const PLATFORM_APP_CANARY_HOSTNAME = "canary.companyincluded.app";
|
|
15
|
+
/**
|
|
16
|
+
* The hostname Archon's uptime probes (`GET /`, 200). Recorded last, only once
|
|
17
|
+
* the canary answered 200 there; cleared first when the canary is removed.
|
|
18
|
+
* Archon reads exactly this key.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PLATFORM_APP_CANARY_HOSTNAME_SCOPE_KEY = "platform_app_canary_hostname";
|
|
21
|
+
/** `{ zoneId, id }` of the route `canary.companyincluded.app/*`. */
|
|
22
|
+
export declare const PLATFORM_APP_CANARY_ROUTE_SCOPE_KEY = "app_canary_route";
|
|
23
|
+
/** `{ zone, id }` of the host's proxied `AAAA 100::` record (the zone has no wildcard). */
|
|
24
|
+
export declare const PLATFORM_APP_CANARY_DNS_SCOPE_KEY = "app_canary_dns";
|
|
25
|
+
export declare const PLATFORM_APP_CANARY_APPLIED_TAG_SCOPE_KEY = "app_canary_applied_tag";
|
|
26
|
+
export declare const PLATFORM_APP_CANARY_APPLIED_SHA256_SCOPE_KEY = "app_canary_applied_sha256";
|
|
27
|
+
export declare const PLATFORM_APP_CANARY_REMOVED_SCOPE_KEY = "app_canary_removed";
|
|
28
|
+
/** The scope id of the script itself, written by `runPlatformSteps` from the step's `resourceId`. */
|
|
29
|
+
export declare const PLATFORM_APP_CANARY_RESOURCE_KEY = "cloudflare:worker:platform:app-canary";
|
|
30
|
+
/** Whether the script is recorded as foundry's and not removed since. */
|
|
31
|
+
export declare function appCanaryDeployed(recordedIds: Readonly<Record<string, string>>): boolean;
|
|
32
|
+
/** Whether anything of the canary is recorded that disabling it must remove. */
|
|
33
|
+
export declare function appCanaryRecorded(recordedIds: Readonly<Record<string, string>>): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The canary step. Blocked, not planned (rule 2), while no artifact is
|
|
36
|
+
* pinned, edge is not applied, or the executor lacks its dependencies or
|
|
37
|
+
* found something in the way (`executorBlocked`). Planned again when the pin
|
|
38
|
+
* changed or a run never got to recording the hostname. When the platform no
|
|
39
|
+
* longer enables it, a destructive step removes whatever of it is recorded.
|
|
40
|
+
*/
|
|
41
|
+
export declare function planAppCanary(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>, executorBlocked: string | undefined, edgeApplied: boolean): {
|
|
42
|
+
step: DraftStep | null;
|
|
43
|
+
blocked: string[];
|
|
44
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { APPS_DOMAIN } from '../org.js';
|
|
2
|
+
/**
|
|
3
|
+
* foundry#1133: the app-layer canary, one `cloudflare:worker` step keyed
|
|
4
|
+
* {@link PLATFORM_APP_CANARY_KEY}. `runPlatformSteps` records the script name
|
|
5
|
+
* under `cloudflare:worker:platform:app-canary` once it succeeds.
|
|
6
|
+
*/
|
|
7
|
+
export const PLATFORM_APP_CANARY_KEY = 'platform:app-canary';
|
|
8
|
+
/** The Worker script's fixed name at Cloudflare, on the platform's own edge organisation. */
|
|
9
|
+
export const PLATFORM_APP_CANARY_SCRIPT_NAME = 'uptime-canary';
|
|
10
|
+
/** The canary's first label under {@link APPS_DOMAIN}; locked for companies (`PLATFORM_APP_LABELS`). */
|
|
11
|
+
export const PLATFORM_APP_CANARY_LABEL = 'canary';
|
|
12
|
+
/** Where the canary answers: one label under the apps zone, as archon#812 requires. */
|
|
13
|
+
export const PLATFORM_APP_CANARY_HOSTNAME = `${PLATFORM_APP_CANARY_LABEL}.${APPS_DOMAIN}`;
|
|
14
|
+
/**
|
|
15
|
+
* The hostname Archon's uptime probes (`GET /`, 200). Recorded last, only once
|
|
16
|
+
* the canary answered 200 there; cleared first when the canary is removed.
|
|
17
|
+
* Archon reads exactly this key.
|
|
18
|
+
*/
|
|
19
|
+
export const PLATFORM_APP_CANARY_HOSTNAME_SCOPE_KEY = 'platform_app_canary_hostname';
|
|
20
|
+
/** `{ zoneId, id }` of the route `canary.companyincluded.app/*`. */
|
|
21
|
+
export const PLATFORM_APP_CANARY_ROUTE_SCOPE_KEY = 'app_canary_route';
|
|
22
|
+
/** `{ zone, id }` of the host's proxied `AAAA 100::` record (the zone has no wildcard). */
|
|
23
|
+
export const PLATFORM_APP_CANARY_DNS_SCOPE_KEY = 'app_canary_dns';
|
|
24
|
+
export const PLATFORM_APP_CANARY_APPLIED_TAG_SCOPE_KEY = 'app_canary_applied_tag';
|
|
25
|
+
export const PLATFORM_APP_CANARY_APPLIED_SHA256_SCOPE_KEY = 'app_canary_applied_sha256';
|
|
26
|
+
export const PLATFORM_APP_CANARY_REMOVED_SCOPE_KEY = 'app_canary_removed';
|
|
27
|
+
/** The scope id of the script itself, written by `runPlatformSteps` from the step's `resourceId`. */
|
|
28
|
+
export const PLATFORM_APP_CANARY_RESOURCE_KEY = `cloudflare:worker:${PLATFORM_APP_CANARY_KEY}`;
|
|
29
|
+
/** Whether the script is recorded as foundry's and not removed since. */
|
|
30
|
+
export function appCanaryDeployed(recordedIds) {
|
|
31
|
+
return (Boolean(recordedIds[PLATFORM_APP_CANARY_RESOURCE_KEY]) &&
|
|
32
|
+
!recordedIds[PLATFORM_APP_CANARY_REMOVED_SCOPE_KEY]);
|
|
33
|
+
}
|
|
34
|
+
/** Whether anything of the canary is recorded that disabling it must remove. */
|
|
35
|
+
export function appCanaryRecorded(recordedIds) {
|
|
36
|
+
return (appCanaryDeployed(recordedIds) ||
|
|
37
|
+
Boolean(recordedIds[PLATFORM_APP_CANARY_ROUTE_SCOPE_KEY]) ||
|
|
38
|
+
Boolean(recordedIds[PLATFORM_APP_CANARY_DNS_SCOPE_KEY]) ||
|
|
39
|
+
Boolean(recordedIds[PLATFORM_APP_CANARY_HOSTNAME_SCOPE_KEY]));
|
|
40
|
+
}
|
|
41
|
+
const BLOCKED = 'the app-layer canary is blocked, not planned';
|
|
42
|
+
/**
|
|
43
|
+
* The canary step. Blocked, not planned (rule 2), while no artifact is
|
|
44
|
+
* pinned, edge is not applied, or the executor lacks its dependencies or
|
|
45
|
+
* found something in the way (`executorBlocked`). Planned again when the pin
|
|
46
|
+
* changed or a run never got to recording the hostname. When the platform no
|
|
47
|
+
* longer enables it, a destructive step removes whatever of it is recorded.
|
|
48
|
+
*/
|
|
49
|
+
export function planAppCanary(platform, recordedIds, executorBlocked, edgeApplied) {
|
|
50
|
+
const canary = platform.appCanary;
|
|
51
|
+
const action = (desired) => ({
|
|
52
|
+
system: 'cloudflare',
|
|
53
|
+
kind: 'worker',
|
|
54
|
+
key: PLATFORM_APP_CANARY_KEY,
|
|
55
|
+
desired,
|
|
56
|
+
});
|
|
57
|
+
if (!canary?.enabled) {
|
|
58
|
+
if (!appCanaryRecorded(recordedIds))
|
|
59
|
+
return { step: null, blocked: [] };
|
|
60
|
+
if (!edgeApplied) {
|
|
61
|
+
return {
|
|
62
|
+
step: null,
|
|
63
|
+
blocked: [`${BLOCKED}: its removal goes through edge, which is not applied`],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (executorBlocked)
|
|
67
|
+
return { step: null, blocked: [executorBlocked] };
|
|
68
|
+
return {
|
|
69
|
+
step: {
|
|
70
|
+
action: action({ remove: true }),
|
|
71
|
+
step: {
|
|
72
|
+
description: `remove the app-layer canary ${PLATFORM_APP_CANARY_SCRIPT_NAME}, its route and record on ${PLATFORM_APP_CANARY_HOSTNAME}, and the hostname Archon's uptime probes`,
|
|
73
|
+
destroys: true,
|
|
74
|
+
migrates: false,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
blocked: [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (!canary.tag || !canary.sha256) {
|
|
81
|
+
return {
|
|
82
|
+
step: null,
|
|
83
|
+
blocked: [
|
|
84
|
+
`${BLOCKED}: no artifact is pinned; declare appCanary.tag and appCanary.sha256 (appCanaryShippedPin() gives the ones this foundry ships)`,
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (!edgeApplied) {
|
|
89
|
+
return { step: null, blocked: [`${BLOCKED}: platform:edge has not been applied yet`] };
|
|
90
|
+
}
|
|
91
|
+
if (executorBlocked)
|
|
92
|
+
return { step: null, blocked: [executorBlocked] };
|
|
93
|
+
const applied = appCanaryDeployed(recordedIds) &&
|
|
94
|
+
recordedIds[PLATFORM_APP_CANARY_APPLIED_TAG_SCOPE_KEY] === canary.tag &&
|
|
95
|
+
recordedIds[PLATFORM_APP_CANARY_APPLIED_SHA256_SCOPE_KEY] === canary.sha256 &&
|
|
96
|
+
recordedIds[PLATFORM_APP_CANARY_HOSTNAME_SCOPE_KEY] === PLATFORM_APP_CANARY_HOSTNAME;
|
|
97
|
+
if (applied)
|
|
98
|
+
return { step: null, blocked: [] };
|
|
99
|
+
return {
|
|
100
|
+
step: {
|
|
101
|
+
action: action({ gitRef: canary.tag, sha256: canary.sha256 }),
|
|
102
|
+
step: {
|
|
103
|
+
description: `deploy the app-layer canary ${PLATFORM_APP_CANARY_SCRIPT_NAME} (${canary.tag}) on ${PLATFORM_APP_CANARY_HOSTNAME}, with no Access gate, and record the hostname once it answers`,
|
|
104
|
+
destroys: false,
|
|
105
|
+
migrates: false,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
blocked: [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -61,10 +61,12 @@ export interface BlueGreenApplyDeps {
|
|
|
61
61
|
guardInputs?: readonly string[];
|
|
62
62
|
/**
|
|
63
63
|
* The company the stage mail sink belongs to (its name is `<slug>-stage-sink`). Absent: no sink is made and
|
|
64
|
-
* stage holds no mail env at all, so the copy stays refused.
|
|
64
|
+
* stage holds no mail env at all, so the copy stays refused. `mailAppKey`, for an entry that sends mail, is
|
|
65
|
+
* the `MAIL_APP_KEY` stage's sends are tagged with (`<key>-work`); it is written with the sink's env.
|
|
65
66
|
*/
|
|
66
67
|
stageSink?: {
|
|
67
68
|
slug: string;
|
|
69
|
+
mailAppKey?: string;
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
@@ -119,7 +121,9 @@ export declare function applyBlueGreenWeights(deps: LbConfigDeps, subject: BlueG
|
|
|
119
121
|
}): Promise<void>;
|
|
120
122
|
/**
|
|
121
123
|
* The primary blue's env for another color: the database host swapped when the server is not the primary, and
|
|
122
|
-
* never the reporting names
|
|
124
|
+
* never the color's own reporting names (`isColorReportingEnv`: color, release, health token) or the actions
|
|
125
|
+
* key, which differ by design. The reporting backend's address and keys (`REPORTING_URL`, `REPORTING_SITE_KEY`,
|
|
126
|
+
* `REPORTING_OPERATOR_KEY`) are the app's, not the color's, so they are copied like any other env. The structure apply and a deploy both
|
|
123
127
|
* copy through this, so a color is never deployed with an older copy than the structure apply wrote.
|
|
124
128
|
*/
|
|
125
129
|
export declare function copiedEnv(deps: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { MAIL_APP_KEY_ENV } from '../org.js';
|
|
2
3
|
import { ACTIONS_KEY_ENV, COLORS, LB_CONFIG_PATH, bgKey, colorAppName, colorBackend, formatWeights, lbAppName, lbDockerfile, parseWeights, prodResources, renderLbConfig, reportingEnv, stageAppName, stageAppUrl, stageKindOf, stageResources, structureHash, } from './blue-green.js';
|
|
3
4
|
import { recordBuiltCommit } from './build-record.js';
|
|
4
5
|
import { withTimeout } from './call-timeout.js';
|
|
@@ -7,6 +8,7 @@ import { waitForNewDeployment } from './deploy-health.js';
|
|
|
7
8
|
import { commandHealthCheckSettings, wgetProbe } from './health-check-settings.js';
|
|
8
9
|
import { ENTRY_CONTAINER_LABELS } from './proxy-labels.js';
|
|
9
10
|
import { remoteDatabaseAddress, remoteDatabaseHost } from './remote-database.js';
|
|
11
|
+
import { isColorReportingEnv } from './reporting.js';
|
|
10
12
|
import { STAGE_SIGN_IN_NAMES, buildStageEnv } from './stage-env.js';
|
|
11
13
|
import { buildStageProbes, runStageProbes, stageProbesRecord, } from './stage-guards.js';
|
|
12
14
|
import { assertStageIsolated, envClass, stageIsolationHash, } from './stage-isolation.js';
|
|
@@ -163,7 +165,9 @@ async function writeLbConfig(deps, subject, server, lbId, weights) {
|
|
|
163
165
|
}
|
|
164
166
|
/**
|
|
165
167
|
* The primary blue's env for another color: the database host swapped when the server is not the primary, and
|
|
166
|
-
* never the reporting names
|
|
168
|
+
* never the color's own reporting names (`isColorReportingEnv`: color, release, health token) or the actions
|
|
169
|
+
* key, which differ by design. The reporting backend's address and keys (`REPORTING_URL`, `REPORTING_SITE_KEY`,
|
|
170
|
+
* `REPORTING_OPERATOR_KEY`) are the app's, not the color's, so they are copied like any other env. The structure apply and a deploy both
|
|
167
171
|
* copy through this, so a color is never deployed with an older copy than the structure apply wrote.
|
|
168
172
|
*/
|
|
169
173
|
export function copiedEnv(deps, subject, source, server) {
|
|
@@ -175,7 +179,7 @@ export function copiedEnv(deps, subject, source, server) {
|
|
|
175
179
|
for (const env of source) {
|
|
176
180
|
if (env.is_preview ||
|
|
177
181
|
env.value === undefined ||
|
|
178
|
-
env.key
|
|
182
|
+
isColorReportingEnv(env.key) ||
|
|
179
183
|
env.key === ACTIONS_KEY_ENV) {
|
|
180
184
|
continue;
|
|
181
185
|
}
|
|
@@ -482,7 +486,14 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
482
486
|
}
|
|
483
487
|
// The sink's mail env, only once the sink app is deployed. Before that stage holds no mail env at all.
|
|
484
488
|
const sinkApplied = deps.stageSink && deps.recorded()[STAGE_SINK_APPLIED_SCOPE_KEY];
|
|
485
|
-
const mail = deps.stageSink && sinkApplied
|
|
489
|
+
const mail = deps.stageSink && sinkApplied
|
|
490
|
+
? [
|
|
491
|
+
...stageMailEnv(deps.stageSink),
|
|
492
|
+
...(deps.stageSink.mailAppKey
|
|
493
|
+
? [{ key: MAIL_APP_KEY_ENV, value: deps.stageSink.mailAppKey }]
|
|
494
|
+
: []),
|
|
495
|
+
]
|
|
496
|
+
: [];
|
|
486
497
|
const stageHas = new Map((await coolify.listEnvs(appId)).filter((e) => !e.is_preview).map((e) => [e.key, e.value]));
|
|
487
498
|
const mailChanged = [...mail, ...signInEnv].some((e) => stageHas.get(e.key) !== e.value);
|
|
488
499
|
stageEnv.push(...mail);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { isFoundryOwnedBrokerEntryName, } from '../org.js';
|
|
2
|
+
import { isFoundryOwnedBrokerEntryName, signsInBoth, signsInCustomers, } from '../org.js';
|
|
3
3
|
import { entryClientIdScopeKey } from './agora.js';
|
|
4
4
|
import { MCP_DATABASE_URL_ENV, MCP_OAUTH_SECRET_ENV } from './broker-mcp.js';
|
|
5
5
|
import { BROKER_WORKSPACES_DATABASE_ENV, wantedWorkspaces, workspacesUrl, } from './entry-workspaces.js';
|
|
@@ -7,6 +7,7 @@ import { entryHostnames } from './hostnames.js';
|
|
|
7
7
|
import { loginHostnameFor } from './login-host.js';
|
|
8
8
|
import { PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, parseBrokerDeployRecord, } from './platform-diff.js';
|
|
9
9
|
import { archonOrgConflicts, brokerDeployNeeded, brokerDeployRecordValue, missingBrokerInputs, validateBrokerAppEntry, } from './platform-providers.js';
|
|
10
|
+
import { registeredAppEnv } from './registered-app-env.js';
|
|
10
11
|
/**
|
|
11
12
|
* F5 (https://github.com/wtfalch/foundry/issues/314): an entry with
|
|
12
13
|
* `uses.auth` gets one entry in the broker's `AUTH_APPS`. The org executor has
|
|
@@ -68,11 +69,23 @@ export function entryAuthEnv(org, entry, recorded, issuer) {
|
|
|
68
69
|
const auth = entry.uses?.auth;
|
|
69
70
|
if (!auth)
|
|
70
71
|
return {};
|
|
71
|
-
const organizationId = auth
|
|
72
|
+
const organizationId = signsInCustomers(auth)
|
|
73
|
+
? recorded[`zitadel:customers:${org.id}`]
|
|
74
|
+
: recorded.zitadel_org;
|
|
75
|
+
// An entry both sign in to holds what the broker answered its registration (ADR 0003), the team's client too.
|
|
76
|
+
const registered = signsInBoth(auth) ? registeredAppEnv(recorded, entry.key) : null;
|
|
72
77
|
const values = {
|
|
73
78
|
AUTH_ORGANIZATION_ID: organizationId,
|
|
74
|
-
AUTH_CLIENT_ID:
|
|
79
|
+
AUTH_CLIENT_ID: signsInBoth(auth)
|
|
80
|
+
? registered?.AUTH_CLIENT_ID
|
|
81
|
+
: recorded[entryClientIdScopeKey(entry.key)],
|
|
75
82
|
AUTH_ISSUER: issuer ?? undefined,
|
|
83
|
+
...(registered
|
|
84
|
+
? {
|
|
85
|
+
AUTH_TEAM_CLIENT_ID: registered.AUTH_TEAM_CLIENT_ID,
|
|
86
|
+
AUTH_TEAM_ORGANIZATION_ID: registered.AUTH_TEAM_ORGANIZATION_ID,
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
76
89
|
};
|
|
77
90
|
const mcp = entryMcp(org, entry, recorded);
|
|
78
91
|
const mcpValues = mcp
|
|
@@ -93,12 +106,14 @@ export function entryAuthEnvAppliedScopeKey(entryKey) {
|
|
|
93
106
|
}
|
|
94
107
|
export function wantedBrokerEntry(org, entry, recorded) {
|
|
95
108
|
const auth = entry.uses?.auth;
|
|
96
|
-
const organizationId = auth
|
|
109
|
+
const organizationId = signsInCustomers(auth)
|
|
110
|
+
? recorded[`zitadel:customers:${org.id}`]
|
|
111
|
+
: recorded.zitadel_org;
|
|
97
112
|
const clientId = recorded[entryClientIdScopeKey(entry.key)];
|
|
98
113
|
const mailDomain = recorded.mail_domain;
|
|
99
114
|
const missing = [];
|
|
100
115
|
if (!organizationId) {
|
|
101
|
-
missing.push(auth
|
|
116
|
+
missing.push(signsInCustomers(auth)
|
|
102
117
|
? { what: 'the customers organisation id', producedBy: ['zitadel', 'customers'] }
|
|
103
118
|
: { what: "the org's ZITADEL org id", producedBy: ['zitadel', 'issuer'] });
|
|
104
119
|
}
|
|
@@ -4,6 +4,7 @@ import { BOULE_DATABASE_SCOPE_KEY } from './boule.js';
|
|
|
4
4
|
import { CHAT_REKEY_MAS_DB_SCOPE_KEY, CHAT_REKEY_SYNAPSE_DB_SCOPE_KEY } from './executor.js';
|
|
5
5
|
import { MAIL_DATABASE_NAME, MAIL_DATABASE_SCOPE_KEY } from './mail-store.js';
|
|
6
6
|
import { serviceOnServer } from './org-postgres.js';
|
|
7
|
+
import { REPORTING_DATABASE_SCOPE_KEY } from './reporting.js';
|
|
7
8
|
import { signinDbBackupMissing } from './signin-db-backup.js';
|
|
8
9
|
import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db-tls.js';
|
|
9
10
|
/**
|
|
@@ -31,6 +32,7 @@ export function companyDatabaseKeys(org) {
|
|
|
31
32
|
chat_mas: 'coolify_chat_mas_database',
|
|
32
33
|
boule: BOULE_DATABASE_SCOPE_KEY,
|
|
33
34
|
analytics: ANALYTICS_DATABASE_SCOPE_KEY,
|
|
35
|
+
reporting: REPORTING_DATABASE_SCOPE_KEY,
|
|
34
36
|
chat_rekey_synapse: CHAT_REKEY_SYNAPSE_DB_SCOPE_KEY,
|
|
35
37
|
chat_rekey_mas: CHAT_REKEY_MAS_DB_SCOPE_KEY,
|
|
36
38
|
postgres: ORG_POSTGRES_SERVER_SCOPE_KEY,
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -66,6 +66,8 @@ export interface PlanOrgOptions {
|
|
|
66
66
|
zitadelOrgMetadata?: boolean;
|
|
67
67
|
/** F5: the executor was built with a `broker` port. */
|
|
68
68
|
broker?: boolean;
|
|
69
|
+
/** ADR 0003: the executor was built with a `registrarApi` port, so an entry both sign in to can be registered. */
|
|
70
|
+
registrarApi?: boolean;
|
|
69
71
|
/** The broker the org's entries are written to has `WORKSPACES_DATABASE_URL`, or a URL is held to give it. Absent reads as false. */
|
|
70
72
|
brokerWorkspacesDatabase?: boolean;
|
|
71
73
|
/** F5b: the broker has none, and the host can make the database and hold its URL (`canProvisionWorkspacesDatabase`). Absent reads as false. */
|
|
@@ -86,6 +88,16 @@ export interface PlanOrgOptions {
|
|
|
86
88
|
keysRegistrarUrl?: string;
|
|
87
89
|
/** The executor was built with an `analytics` port; without it each `uses.analytics` key is blocked. */
|
|
88
90
|
analyticsPort?: boolean;
|
|
91
|
+
/** The executor was built with a `reporting` port; without it each `uses.reporting` key is blocked. */
|
|
92
|
+
reportingPort?: boolean;
|
|
93
|
+
/** foundry#1119: the executor's jobs scheduler port mints and revokes project keys; without it Boule's key per `uses.jobs` entry is blocked. */
|
|
94
|
+
jobsProjectKeys?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* foundry#1122: ai offers a company Boule's five read permissions (`ai-boule-key.ts`). With it the organisation
|
|
97
|
+
* is offered them, the org's own ai key is minted again with them, and Boule's key is minted below it; without
|
|
98
|
+
* it Boule's key is blocked.
|
|
99
|
+
*/
|
|
100
|
+
aiBouleReads?: boolean;
|
|
89
101
|
/** C2: true when the executor was built with a `WorkerAppDatabasePort`; a `workerApp.database` is blocked without it. */
|
|
90
102
|
workerAppDatabase?: boolean;
|
|
91
103
|
/** True when that port has `deletableTables`; a `workerApp.database.noDelete` is blocked without it. */
|
|
@@ -286,6 +298,16 @@ export interface OrgPostgresPlan {
|
|
|
286
298
|
export declare const AI_KEY_UNDELIVERED_SCOPE_KEY = "ai_key_undelivered";
|
|
287
299
|
/** Restated from `executor.ts`, the same rule: the id of a jobs project registered but not delivered. */
|
|
288
300
|
export declare const JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY = "jobs_registration_undelivered";
|
|
301
|
+
/**
|
|
302
|
+
* Deduped `ai.*` permissions offered to every organisation by default
|
|
303
|
+
* (Decision, this campaign's report): the minimum set that lets an org
|
|
304
|
+
* actually run jobs against the platform's own offered models --
|
|
305
|
+
* `ai.offerings:invoke` to invoke an offering, `ai.runs:create` to submit the
|
|
306
|
+
* run itself. `ai/packages/service/src/catalogue.ts`'s five `offered: true`
|
|
307
|
+
* permissions also include the three `ai.connections:*` ones, which let an
|
|
308
|
+
* organisation bring its own provider credentials (BYOK) -- a separate,
|
|
309
|
+
* explicit opt-in this default deliberately leaves out.
|
|
310
|
+
*/
|
|
289
311
|
/**
|
|
290
312
|
* #484: the spend cap foundry applies when an org declares no AI limit
|
|
291
313
|
* (`org.ai.limitMicros === null`) -- Archon creates every company that way,
|