@wtfalch/foundry 0.70.0 → 0.70.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/declare.js +2 -0
- package/dist/migrations/0070_platform_usage_tail.sql +12 -0
- package/dist/plan/diff.js +23 -7
- package/dist/plan/edge-access.d.ts +9 -0
- package/dist/plan/edge-access.js +20 -0
- package/dist/plan/executor.js +25 -8
- package/dist/plan/own-diff.js +4 -1
- package/dist/plan/plan-apply.d.ts +2 -0
- package/dist/plan/platform-diff.d.ts +62 -0
- package/dist/plan/platform-diff.js +165 -4
- package/dist/plan/platform-providers.d.ts +16 -0
- package/dist/plan/platform-providers.js +86 -4
- package/dist/plan/usage-tail-worker.d.ts +29 -0
- package/dist/plan/usage-tail-worker.js +105 -0
- package/dist/platform.d.ts +20 -0
- package/dist/platform.js +14 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +1 -1
|
@@ -170,6 +170,8 @@ export function declaredPlatformFor(apex, serverIpv4) {
|
|
|
170
170
|
// only after Archon runs. Declared on with no pin, it would be reported
|
|
171
171
|
// blocked and the bootstrap would never be complete.
|
|
172
172
|
uptime: { enabled: false, tag: '', sha256: '' },
|
|
173
|
+
// foundry#1024: off until a human pins an edge release tag and digest, the same reason as uptime.
|
|
174
|
+
usageTail: { enabled: false, tag: '', sha256: '' },
|
|
173
175
|
heartbeatsProvider: 'self-hosted',
|
|
174
176
|
};
|
|
175
177
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the usage tail Worker on the declared platform (foundry#1024).
|
|
2
|
+
-- Mirrors `src/store/schema.ts`'s `foundryDeclaredPlatform` additions.
|
|
3
|
+
-- `usage_tail_tag` and `usage_tail_sha256` are the pin of the edge release
|
|
4
|
+
-- whose built Worker foundry deploys; both empty means nothing is pinned
|
|
5
|
+
-- yet, and the planner reports that instead of planning the step. Unlike the
|
|
6
|
+
-- uptime Worker, this one is off by default: nothing has pinned a release
|
|
7
|
+
-- yet on any existing platform.
|
|
8
|
+
|
|
9
|
+
alter table foundry_declared_platform
|
|
10
|
+
add column if not exists usage_tail_enabled boolean not null default false,
|
|
11
|
+
add column if not exists usage_tail_tag text not null default '',
|
|
12
|
+
add column if not exists usage_tail_sha256 text not null default '';
|
package/dist/plan/diff.js
CHANGED
|
@@ -176,6 +176,18 @@ function liveResourceName(r) {
|
|
|
176
176
|
const type = r.kind === 'dns_record' && typeof r.config.type === 'string' ? `${r.config.type} ` : '';
|
|
177
177
|
return `${type}${name}, ${r.key}`;
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* The provider's own name for a live resource, for a park or provision
|
|
181
|
+
* step's own prose (#1062, pain point 3): an operator told to "park the
|
|
182
|
+
* database" acted on the company's Coolify server, named after the same
|
|
183
|
+
* company, instead of the database resource the step meant. `null` when
|
|
184
|
+
* there is no live resource yet, or it carries no name -- unlike
|
|
185
|
+
* {@link liveResourceName} above, this never falls back to the bare key,
|
|
186
|
+
* since an id is not a name a step's own text should ever show.
|
|
187
|
+
*/
|
|
188
|
+
function liveName(live) {
|
|
189
|
+
return typeof live?.config.name === 'string' && live.config.name !== '' ? live.config.name : null;
|
|
190
|
+
}
|
|
179
191
|
/**
|
|
180
192
|
* The diff engine: #19's "a plan reads live state from Zitadel, Coolify and
|
|
181
193
|
* Cloudflare and diffs it against the declared org", built on #10's rule
|
|
@@ -3166,7 +3178,7 @@ function planKosmos(org, byKey, claimed, recordedIds, issuer, otherSlugs) {
|
|
|
3166
3178
|
if (!kosmosOn(org)) {
|
|
3167
3179
|
// Switched off: park a live app (stop it, keep the database), like CMS and calls. A stopped app is
|
|
3168
3180
|
// already parked, so the plan reaches empty.
|
|
3169
|
-
const parked = planHostingToggle('coolify', 'kosmos', false, org, byKey, claimed, () => `provision ${org.slug}'s kosmos`, () => `park ${org.slug}'s kosmos (stops serving; its database is retained, not deleted)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
3181
|
+
const parked = planHostingToggle('coolify', 'kosmos', false, org, byKey, claimed, () => `provision ${org.slug}'s kosmos`, (live) => `park ${org.slug}'s kosmos${liveName(live) ? ` ${liveName(live)}` : ''} (stops serving; its database is retained, not deleted)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
3170
3182
|
return { step: parked, blocked: null };
|
|
3171
3183
|
}
|
|
3172
3184
|
const key = resourceKey('coolify', 'kosmos', org.id);
|
|
@@ -3533,7 +3545,11 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
3533
3545
|
* park-not-delete rule (#7 corrections 1) -- turning it off never deletes
|
|
3534
3546
|
* the org's own app, database or tunnel configuration, it parks it.
|
|
3535
3547
|
*/
|
|
3536
|
-
function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed,
|
|
3548
|
+
function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed,
|
|
3549
|
+
// #1062: takes the live resource so a caller can name it (Coolify's own
|
|
3550
|
+
// name for it, from `config.name`), rather than a generic noun a park or
|
|
3551
|
+
// provision step's own text can be told apart from the server it runs on.
|
|
3552
|
+
describeProvision, describePark, stepKey = org.id, isOn = (live) => live.config.enabled !== false) {
|
|
3537
3553
|
const key = resourceKey(system, kind, stepKey);
|
|
3538
3554
|
const live = byKey.get(key);
|
|
3539
3555
|
// A live resource is on unless it says it is parked. Providers' own
|
|
@@ -3551,7 +3567,7 @@ function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed, de
|
|
|
3551
3567
|
return null;
|
|
3552
3568
|
return {
|
|
3553
3569
|
action: { system, kind, key: stepKey, desired: { enabled: false } },
|
|
3554
|
-
step: { description: describePark(), destroys: true, migrates: false },
|
|
3570
|
+
step: { description: describePark(live), destroys: true, migrates: false },
|
|
3555
3571
|
};
|
|
3556
3572
|
}
|
|
3557
3573
|
if (live)
|
|
@@ -3562,7 +3578,7 @@ function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed, de
|
|
|
3562
3578
|
return null;
|
|
3563
3579
|
return {
|
|
3564
3580
|
action: { system, kind, key: stepKey, desired: { enabled: true } },
|
|
3565
|
-
step: { description: describeProvision(), destroys: false, migrates: false },
|
|
3581
|
+
step: { description: describeProvision(live), destroys: false, migrates: false },
|
|
3566
3582
|
};
|
|
3567
3583
|
}
|
|
3568
3584
|
/**
|
|
@@ -3701,7 +3717,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
3701
3717
|
});
|
|
3702
3718
|
}
|
|
3703
3719
|
const refused = noRepo || composeBuilt;
|
|
3704
|
-
const toggled = planHostingToggle('coolify', 'app', entry !== undefined && !refused, org, byKey, claimed, () => `provision ${org.slug}'s ${label}${volumesNote(entry)}`, () => `park ${org.slug}'s ${label} (stops serving; app data is retained, not deleted -- #7 corrections 1)`, stepKey);
|
|
3720
|
+
const toggled = planHostingToggle('coolify', 'app', entry !== undefined && !refused, org, byKey, claimed, () => `provision ${org.slug}'s ${label}${volumesNote(entry)}`, (live) => `park ${org.slug}'s ${label}${liveName(live) ? ` ${liveName(live)}` : ''} (stops serving; app data is retained, not deleted -- #7 corrections 1)`, stepKey);
|
|
3705
3721
|
// #447: only the live app's own `git_repository` decides, never the
|
|
3706
3722
|
// declared entry (a `repo: null` entry can sit over a bespoke app, which is
|
|
3707
3723
|
// parked as before). Only the `app` key ever held the default app, and an
|
|
@@ -4253,7 +4269,7 @@ function planDatabase(org, byKey, claimed, recordedIds) {
|
|
|
4253
4269
|
if (!appHome || !legacy || !recordedIds[legacy] || serviceOnServer(org, appHome, recordedIds)) {
|
|
4254
4270
|
return null;
|
|
4255
4271
|
}
|
|
4256
|
-
return planHostingToggle('coolify', 'database', org.hosting.entries.some(entryHasDatabase), org, byKey, claimed, () => `provision a database for ${org.slug}'s app`, () => `park ${org.slug}'s database (data is retained, not deleted -- #7 corrections 1)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
4272
|
+
return planHostingToggle('coolify', 'database', org.hosting.entries.some(entryHasDatabase), org, byKey, claimed, () => `provision a database for ${org.slug}'s app`, (live) => `park ${org.slug}'s database${liveName(live) ? ` ${liveName(live)}` : ''} (data is retained, not deleted -- #7 corrections 1)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
4257
4273
|
}
|
|
4258
4274
|
function sortedNames(names) {
|
|
4259
4275
|
return names.filter(Boolean).sort().join(',');
|
|
@@ -4298,7 +4314,7 @@ function planTunnel(org, byKey, claimed, recordedIds, platformApex) {
|
|
|
4298
4314
|
appsOn(org) ||
|
|
4299
4315
|
// A company without a domain still gets a tunnel of its own for an entry at `<slug>.oikos.host`.
|
|
4300
4316
|
entryRoutes(org, recordedIds).length > 0;
|
|
4301
|
-
const toggled = planHostingToggle('cloudflare', 'tunnel', desired, org, byKey, claimed, () => `provision a Cloudflare tunnel for ${org.slug}'s app`, () => `park ${org.slug}'s Cloudflare tunnel (configuration is retained, not deleted -- #7 corrections 1)`, org.id,
|
|
4317
|
+
const toggled = planHostingToggle('cloudflare', 'tunnel', desired, org, byKey, claimed, () => `provision a Cloudflare tunnel for ${org.slug}'s app`, (live) => `park ${org.slug}'s Cloudflare tunnel${liveName(live) ? ` ${liveName(live)}` : ''} (configuration is retained, not deleted -- #7 corrections 1)`, org.id,
|
|
4302
4318
|
// Cloudflare's status, read against what is declared: a park cannot change
|
|
4303
4319
|
// it, so judging on/off by status alone would never let the plan empty.
|
|
4304
4320
|
// Wanted on: only `down` needs a repair (`inactive` is a fresh tunnel
|
|
@@ -115,6 +115,15 @@ export interface AccessGateInput {
|
|
|
115
115
|
emails: string[];
|
|
116
116
|
}
|
|
117
117
|
export declare const isNotFound: (error: unknown) => boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Throws the operator-facing message when `error` is edge's generic 404 for
|
|
120
|
+
* a route this SDK expects but the running release predates (#1061, pain
|
|
121
|
+
* point 12: production edge six days behind the SDK failed foundry's first
|
|
122
|
+
* apps step with the raw "Route not found" and no hint why). Returns
|
|
123
|
+
* otherwise, so the caller's own handling of `error` -- a conflict branch,
|
|
124
|
+
* an `isNotFound`/`isNotFoundError` "already gone" check -- still runs.
|
|
125
|
+
*/
|
|
126
|
+
export declare function edgeTooOld(error: unknown, what: string): void;
|
|
118
127
|
/**
|
|
119
128
|
* Edge answers a policy, delete or create call for a live Access app with no
|
|
120
129
|
* edge row `not_found` or 409 `exists` ("create or adopt it first"). So the
|
package/dist/plan/edge-access.js
CHANGED
|
@@ -3,6 +3,26 @@ import { EDGE_DNS_WRITE_CONCURRENCY, limiter, rateLimitRetrier, } from './edge-d
|
|
|
3
3
|
export const isNotFound = (error) => error instanceof EdgeApiError && error.code === 'not_found';
|
|
4
4
|
/** Edge's 409 for a live app on the domain with no edge row. */
|
|
5
5
|
const isExists = (error) => error instanceof EdgeApiError && error.code === 'conflict' && error.reason === 'exists';
|
|
6
|
+
/**
|
|
7
|
+
* Edge's generic 404 fallback for a route no release of it answers yet
|
|
8
|
+
* (`service.ts`'s `errorResponse('not_found', 'Route not found')`), as
|
|
9
|
+
* opposed to a resource-specific 404 ("Key not found" and the like), which
|
|
10
|
+
* never carries this exact message.
|
|
11
|
+
*/
|
|
12
|
+
const isRouteMissing = (error) => error instanceof EdgeApiError && error.status === 404 && error.message === 'Route not found';
|
|
13
|
+
/**
|
|
14
|
+
* Throws the operator-facing message when `error` is edge's generic 404 for
|
|
15
|
+
* a route this SDK expects but the running release predates (#1061, pain
|
|
16
|
+
* point 12: production edge six days behind the SDK failed foundry's first
|
|
17
|
+
* apps step with the raw "Route not found" and no hint why). Returns
|
|
18
|
+
* otherwise, so the caller's own handling of `error` -- a conflict branch,
|
|
19
|
+
* an `isNotFound`/`isNotFoundError` "already gone" check -- still runs.
|
|
20
|
+
*/
|
|
21
|
+
export function edgeTooOld(error, what) {
|
|
22
|
+
if (isRouteMissing(error)) {
|
|
23
|
+
throw new Error(`${what} is blocked: the running edge has no route for this step; deploy edge to the release foundry expects`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
27
|
* Edge answers a policy, delete or create call for a live Access app with no
|
|
8
28
|
* edge row `not_found` or 409 `exists` ("create or adopt it first"). So the
|
package/dist/plan/executor.js
CHANGED
|
@@ -35,7 +35,7 @@ import { DESKTOP_AUTH_APP_SCOPE_KEY, DESKTOP_AUTH_DEPLOYED_SCOPE_KEY, DESKTOP_AU
|
|
|
35
35
|
import { desktopCallbackUri, signInCallbackAppliedValue, signInRedirectUris, unionRedirectUris, zitadelAppDesktopCallbackAppliedScopeKey, } from './desktop-callback.js';
|
|
36
36
|
import { PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, onPlatformNetwork, placed, placedByScope, placedOnPlatform, } from './destinations.js';
|
|
37
37
|
import { DEFAULT_AI_LIMIT_MICROS } from './diff.js';
|
|
38
|
-
import { createEdgeAccess, isNotFound as isEdgeNotFound, } from './edge-access.js';
|
|
38
|
+
import { createEdgeAccess, edgeTooOld, isNotFound as isEdgeNotFound, } from './edge-access.js';
|
|
39
39
|
import { EDGE_DNS_WRITE_CONCURRENCY, createEdgeDns, limiter, } from './edge-dns.js';
|
|
40
40
|
import { createEdgeRateLimits, } from './edge-ratelimits.js';
|
|
41
41
|
import { createEdgeTunnels } from './edge-tunnels.js';
|
|
@@ -10853,6 +10853,7 @@ export class FoundryStepExecutor {
|
|
|
10853
10853
|
await edge.revokeKey(org.id, id);
|
|
10854
10854
|
}
|
|
10855
10855
|
catch (error) {
|
|
10856
|
+
edgeTooOld(error, at);
|
|
10856
10857
|
// A 404 means it is already gone -- recorded as retired below,
|
|
10857
10858
|
// the same as a successful revoke. Anything else is a real
|
|
10858
10859
|
// failure: thrown, not swallowed, so the key stays live and the
|
|
@@ -10922,18 +10923,26 @@ export class FoundryStepExecutor {
|
|
|
10922
10923
|
await edge.revokeKey(org.id, staleUndelivered);
|
|
10923
10924
|
}
|
|
10924
10925
|
catch (error) {
|
|
10926
|
+
edgeTooOld(error, at);
|
|
10925
10927
|
if (!isNotFoundError(error))
|
|
10926
10928
|
throw error;
|
|
10927
10929
|
}
|
|
10928
10930
|
context.recordScopeId(entryEdgeUndeliveredScopeKey(entryKey), '');
|
|
10929
10931
|
await context.persistScopeIds();
|
|
10930
10932
|
}
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10933
|
+
let minted;
|
|
10934
|
+
try {
|
|
10935
|
+
minted = await edge.mintKey({
|
|
10936
|
+
organisationId: org.id,
|
|
10937
|
+
domain: edgeKeyDomain(org),
|
|
10938
|
+
name: desired.name,
|
|
10939
|
+
expiresDays: EDGE_ENTRY_KEY_DAYS,
|
|
10940
|
+
});
|
|
10941
|
+
}
|
|
10942
|
+
catch (e) {
|
|
10943
|
+
edgeTooOld(e, at);
|
|
10944
|
+
throw e;
|
|
10945
|
+
}
|
|
10937
10946
|
context.recordScopeId(entryEdgeUndeliveredScopeKey(entryKey), minted.id);
|
|
10938
10947
|
await context.persistScopeIds();
|
|
10939
10948
|
const envs = [
|
|
@@ -11094,6 +11103,7 @@ export class FoundryStepExecutor {
|
|
|
11094
11103
|
await edge.setOrganisationSlug(org.id, slug);
|
|
11095
11104
|
}
|
|
11096
11105
|
catch (e) {
|
|
11106
|
+
edgeTooOld(e, `edge:slug for ${org.slug}`);
|
|
11097
11107
|
if (e instanceof EdgeApiError && e.code === 'conflict') {
|
|
11098
11108
|
throw new Error(`edge:slug for ${org.slug} is blocked: edge says another company already holds the slug '${slug}', so its platform hostnames cannot be accepted; change the slug`);
|
|
11099
11109
|
}
|
|
@@ -11113,7 +11123,13 @@ export class FoundryStepExecutor {
|
|
|
11113
11123
|
throw new Error(`edge:reserved-labels for ${org.slug}: this executor was built without an EdgePort (#317)`);
|
|
11114
11124
|
}
|
|
11115
11125
|
const labels = action.desired.labels;
|
|
11116
|
-
|
|
11126
|
+
try {
|
|
11127
|
+
await edge.setReservedLabels(org.id, labels);
|
|
11128
|
+
}
|
|
11129
|
+
catch (e) {
|
|
11130
|
+
edgeTooOld(e, `edge:reserved-labels for ${org.slug}`);
|
|
11131
|
+
throw e;
|
|
11132
|
+
}
|
|
11117
11133
|
context.recordScopeId(EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY, JSON.stringify(labels));
|
|
11118
11134
|
return {};
|
|
11119
11135
|
}
|
|
@@ -11658,6 +11674,7 @@ export class FoundryStepExecutor {
|
|
|
11658
11674
|
await edge.revokeKey(org.id, target.id);
|
|
11659
11675
|
}
|
|
11660
11676
|
catch (error) {
|
|
11677
|
+
edgeTooOld(error, `edge:key '${target.key}' for ${org.slug}`);
|
|
11661
11678
|
// Already gone -- the same already-retired key a retire step
|
|
11662
11679
|
// handled before org destroy ever ran can still reach here
|
|
11663
11680
|
// (#317 review); a 404 is success, not a reason to block
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -11,7 +11,7 @@ import { ownHostnames } from './own-hostnames.js';
|
|
|
11
11
|
import { JOBS_PUBLIC_URL_SCOPE_KEY, OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY } from './own-jobs-route.js';
|
|
12
12
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
|
|
13
13
|
import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
|
|
14
|
-
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
14
|
+
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
15
15
|
import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
|
|
16
16
|
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
17
17
|
import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn } from './sign-in.js';
|
|
@@ -217,6 +217,9 @@ export function planOwnAuth(options) {
|
|
|
217
217
|
recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY] &&
|
|
218
218
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
219
219
|
recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
|
|
220
|
+
// The login apps' mail env (`LOGIN_MAIL_FROM`, the Lettermint key); a stack from before it is written again.
|
|
221
|
+
recordedIds[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] ===
|
|
222
|
+
authLoginEnvMarker(`noreply@${names.mailDomain}`) &&
|
|
220
223
|
// The code mail carries the company's name and design; an older apply, or a renamed company, writes it again.
|
|
221
224
|
(options.signinMailWritable === false ||
|
|
222
225
|
recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] === signinMailMarker(org.displayName)) &&
|
|
@@ -337,6 +337,8 @@ export interface PlatformStepExecutor {
|
|
|
337
337
|
authExecutorMissing?: boolean;
|
|
338
338
|
/** #216 round 5: see `PlanPlatformOptions.blocked.authOtherInputsMissing`. */
|
|
339
339
|
authOtherInputsMissing?: string[];
|
|
340
|
+
/** The sealed mail key resolved: `planAuth` asks for the login env marker only when it did. */
|
|
341
|
+
authMailKeyHeld?: boolean;
|
|
340
342
|
files?: string;
|
|
341
343
|
integrations?: string;
|
|
342
344
|
ai?: string;
|
|
@@ -68,6 +68,20 @@ export declare const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = "zitadel_plat
|
|
|
68
68
|
export declare const AUTH_TUNNEL_ROUTES_VERSION = "2";
|
|
69
69
|
/** The {@link AUTH_TUNNEL_ROUTES_VERSION} the sign-in tunnel was last written with. */
|
|
70
70
|
export declare const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = "auth_tunnel_routes_applied";
|
|
71
|
+
/**
|
|
72
|
+
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
73
|
+
* `LETTERMINT_API_TOKEN`). Raise it when a key is added or its meaning changes, so an applied
|
|
74
|
+
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
75
|
+
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
76
|
+
*/
|
|
77
|
+
export declare const AUTH_LOGIN_ENV_VERSION = "1";
|
|
78
|
+
/**
|
|
79
|
+
* What the login apps' env is written for: the version and the sender address (`LOGIN_MAIL_FROM`).
|
|
80
|
+
* The address is no secret; a new mail domain gives a new marker, so the step is planned again.
|
|
81
|
+
*/
|
|
82
|
+
export declare function authLoginEnvMarker(smtpSender: string): string;
|
|
83
|
+
/** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
|
|
84
|
+
export declare const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = "auth_login_env_applied";
|
|
71
85
|
/**
|
|
72
86
|
* `'1'` once the broker holds `MCP_DATABASE_URL` and `MCP_OAUTH_SECRET` (auth ADR 0002), recorded by the
|
|
73
87
|
* sign-in step when an entry of that broker declares `uses.auth.mcp`. A broker entry with `mcp` is not
|
|
@@ -977,6 +991,8 @@ export declare const PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY = "edge_platform_k
|
|
|
977
991
|
*/
|
|
978
992
|
export declare const PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY = "edge_applied_hostname";
|
|
979
993
|
export declare const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = "edge_applied_git_ref";
|
|
994
|
+
/** foundry#1024: what `provisionPlatformEdge` last wrote for {@link usageTailEdgeEnv}; see {@link usageTailEdgeEnvValue}. */
|
|
995
|
+
export declare const PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY = "edge_applied_usage_tail";
|
|
980
996
|
/**
|
|
981
997
|
* F4.dns: where edge answers on the Coolify network
|
|
982
998
|
* (`http://<custom_internal_name>:3000`), recorded by the edge step once the
|
|
@@ -1206,6 +1222,45 @@ export declare const PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY = "uptime_access_
|
|
|
1206
1222
|
export declare function uptimeAccessTokenConflict(): string;
|
|
1207
1223
|
/** Whether anything of the uptime Worker is recorded that disabling it must remove. */
|
|
1208
1224
|
export declare function uptimeRecorded(recordedIds: Readonly<Record<string, string>>): boolean;
|
|
1225
|
+
/**
|
|
1226
|
+
* foundry#1024: the usage tail Worker that meters every company Worker's
|
|
1227
|
+
* use, one `cloudflare:worker` step keyed {@link PLATFORM_USAGE_TAIL_KEY}.
|
|
1228
|
+
* `runPlatformSteps` records the script name under
|
|
1229
|
+
* `cloudflare:worker:platform:usage-tail` once it succeeds, which is what
|
|
1230
|
+
* stops `planUsageTail` planning it again.
|
|
1231
|
+
*/
|
|
1232
|
+
export declare const PLATFORM_USAGE_TAIL_KEY = "platform:usage-tail";
|
|
1233
|
+
/** The Worker script's name at Cloudflare. */
|
|
1234
|
+
export declare const PLATFORM_USAGE_TAIL_SCRIPT_NAME = "platform-usage-tail";
|
|
1235
|
+
/**
|
|
1236
|
+
* The held ref of the Worker's one secret, `EDGE_USAGE_KEY` -- the narrow
|
|
1237
|
+
* `usage:ingest` credential edge's ingest route accepts. Minted once (rule
|
|
1238
|
+
* 1): the hash is never secret, so (the same pattern `provisionPlatformEdge`
|
|
1239
|
+
* uses for `EDGE_PLATFORM_KEY_HASH`) it doubles as both the undelivered
|
|
1240
|
+
* marker and the final recorded value, with nothing extra to compute or a
|
|
1241
|
+
* live read that could fall back to a blank value.
|
|
1242
|
+
*/
|
|
1243
|
+
export declare const PLATFORM_USAGE_TAIL_KEY_REF = "usage-tail:worker:key";
|
|
1244
|
+
export declare const PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY = "usage_tail_key_undelivered";
|
|
1245
|
+
export declare const PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY = "usage_tail_key_set";
|
|
1246
|
+
export declare const PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY = "usage_tail_applied_tag";
|
|
1247
|
+
export declare const PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY = "usage_tail_applied_sha256";
|
|
1248
|
+
export declare const PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY = "usage_tail_removed";
|
|
1249
|
+
/**
|
|
1250
|
+
* What `platform:edge`'s own env should hold for the usage tail Worker
|
|
1251
|
+
* (`USAGE_TAIL_WORKER`, `EDGE_USAGE_KEY_HASH`), or `null` while the Worker is
|
|
1252
|
+
* not deployed or its key is not held yet. `provisionPlatformEdge` writes
|
|
1253
|
+
* these two names on every run alongside `EDGE_PLATFORM_KEY_HASH`; `planEdge`
|
|
1254
|
+
* reads {@link usageTailEdgeEnvValue} to replan edge once this changes.
|
|
1255
|
+
*/
|
|
1256
|
+
export declare function usageTailEdgeEnv(recordedIds: Readonly<Record<string, string>>): {
|
|
1257
|
+
USAGE_TAIL_WORKER: string;
|
|
1258
|
+
EDGE_USAGE_KEY_HASH: string;
|
|
1259
|
+
} | null;
|
|
1260
|
+
/** The value `provisionPlatformEdge` records once it has written {@link usageTailEdgeEnv} to edge's env; empty while there is nothing to write. */
|
|
1261
|
+
export declare function usageTailEdgeEnvValue(recordedIds: Readonly<Record<string, string>>): string;
|
|
1262
|
+
/** Whether anything of the usage tail Worker is recorded that disabling it must remove. */
|
|
1263
|
+
export declare function usageTailRecorded(recordedIds: Readonly<Record<string, string>>): boolean;
|
|
1209
1264
|
/**
|
|
1210
1265
|
* Options for {@link planPlatform}, replacing four positional arguments that
|
|
1211
1266
|
* shifted on every merge that added a platform service (#186, #187, #188) --
|
|
@@ -1265,6 +1320,11 @@ export interface PlanPlatformOptions {
|
|
|
1265
1320
|
* these. Default `[]`.
|
|
1266
1321
|
*/
|
|
1267
1322
|
authOtherInputsMissing?: string[];
|
|
1323
|
+
/**
|
|
1324
|
+
* Whether the sealed Lettermint key resolved. `provisionAuthStack` records the login env marker only
|
|
1325
|
+
* then, so `planAuth` asks for the marker only when this is not false. Unset: asked for.
|
|
1326
|
+
*/
|
|
1327
|
+
authMailKeyHeld?: boolean;
|
|
1268
1328
|
/** C3: why the owner's invite cannot be made (no work address, no mail port). The rest of the step still plans. */
|
|
1269
1329
|
authOwnerInvite?: string;
|
|
1270
1330
|
files?: string;
|
|
@@ -1341,6 +1401,8 @@ export interface PlanPlatformOptions {
|
|
|
1341
1401
|
pending: boolean;
|
|
1342
1402
|
blocked?: string;
|
|
1343
1403
|
};
|
|
1404
|
+
/** foundry#1024: why the usage tail Worker is blocked, or absent when it is not. */
|
|
1405
|
+
usageTail?: string;
|
|
1344
1406
|
/** Why the organisation's release webhook step cannot run (the token is not held), or absent. */
|
|
1345
1407
|
githubReleaseHook?: string;
|
|
1346
1408
|
/**
|
|
@@ -75,6 +75,22 @@ export const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = 'zitadel_platform_aut
|
|
|
75
75
|
export const AUTH_TUNNEL_ROUTES_VERSION = '2';
|
|
76
76
|
/** The {@link AUTH_TUNNEL_ROUTES_VERSION} the sign-in tunnel was last written with. */
|
|
77
77
|
export const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = 'auth_tunnel_routes_applied';
|
|
78
|
+
/**
|
|
79
|
+
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
80
|
+
* `LETTERMINT_API_TOKEN`). Raise it when a key is added or its meaning changes, so an applied
|
|
81
|
+
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
82
|
+
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
83
|
+
*/
|
|
84
|
+
export const AUTH_LOGIN_ENV_VERSION = '1';
|
|
85
|
+
/**
|
|
86
|
+
* What the login apps' env is written for: the version and the sender address (`LOGIN_MAIL_FROM`).
|
|
87
|
+
* The address is no secret; a new mail domain gives a new marker, so the step is planned again.
|
|
88
|
+
*/
|
|
89
|
+
export function authLoginEnvMarker(smtpSender) {
|
|
90
|
+
return `${AUTH_LOGIN_ENV_VERSION}:${smtpSender}`;
|
|
91
|
+
}
|
|
92
|
+
/** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
|
|
93
|
+
export const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = 'auth_login_env_applied';
|
|
78
94
|
/**
|
|
79
95
|
* `'1'` once the broker holds `MCP_DATABASE_URL` and `MCP_OAUTH_SECRET` (auth ADR 0002), recorded by the
|
|
80
96
|
* sign-in step when an entry of that broker declares `uses.auth.mcp`. A broker entry with `mcp` is not
|
|
@@ -1112,6 +1128,8 @@ export const PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY = 'edge_platform_key_set';
|
|
|
1112
1128
|
*/
|
|
1113
1129
|
export const PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY = 'edge_applied_hostname';
|
|
1114
1130
|
export const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = 'edge_applied_git_ref';
|
|
1131
|
+
/** foundry#1024: what `provisionPlatformEdge` last wrote for {@link usageTailEdgeEnv}; see {@link usageTailEdgeEnvValue}. */
|
|
1132
|
+
export const PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY = 'edge_applied_usage_tail';
|
|
1115
1133
|
/**
|
|
1116
1134
|
* F4.dns: where edge answers on the Coolify network
|
|
1117
1135
|
* (`http://<custom_internal_name>:3000`), recorded by the edge step once the
|
|
@@ -1396,6 +1414,56 @@ export function uptimeRecorded(recordedIds) {
|
|
|
1396
1414
|
Boolean(recordedIds[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY]) ||
|
|
1397
1415
|
Boolean(recordedIds[PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY]));
|
|
1398
1416
|
}
|
|
1417
|
+
/**
|
|
1418
|
+
* foundry#1024: the usage tail Worker that meters every company Worker's
|
|
1419
|
+
* use, one `cloudflare:worker` step keyed {@link PLATFORM_USAGE_TAIL_KEY}.
|
|
1420
|
+
* `runPlatformSteps` records the script name under
|
|
1421
|
+
* `cloudflare:worker:platform:usage-tail` once it succeeds, which is what
|
|
1422
|
+
* stops `planUsageTail` planning it again.
|
|
1423
|
+
*/
|
|
1424
|
+
export const PLATFORM_USAGE_TAIL_KEY = 'platform:usage-tail';
|
|
1425
|
+
/** The Worker script's name at Cloudflare. */
|
|
1426
|
+
export const PLATFORM_USAGE_TAIL_SCRIPT_NAME = 'platform-usage-tail';
|
|
1427
|
+
/**
|
|
1428
|
+
* The held ref of the Worker's one secret, `EDGE_USAGE_KEY` -- the narrow
|
|
1429
|
+
* `usage:ingest` credential edge's ingest route accepts. Minted once (rule
|
|
1430
|
+
* 1): the hash is never secret, so (the same pattern `provisionPlatformEdge`
|
|
1431
|
+
* uses for `EDGE_PLATFORM_KEY_HASH`) it doubles as both the undelivered
|
|
1432
|
+
* marker and the final recorded value, with nothing extra to compute or a
|
|
1433
|
+
* live read that could fall back to a blank value.
|
|
1434
|
+
*/
|
|
1435
|
+
export const PLATFORM_USAGE_TAIL_KEY_REF = 'usage-tail:worker:key';
|
|
1436
|
+
export const PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY = 'usage_tail_key_undelivered';
|
|
1437
|
+
export const PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY = 'usage_tail_key_set';
|
|
1438
|
+
export const PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY = 'usage_tail_applied_tag';
|
|
1439
|
+
export const PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY = 'usage_tail_applied_sha256';
|
|
1440
|
+
export const PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY = 'usage_tail_removed';
|
|
1441
|
+
/**
|
|
1442
|
+
* What `platform:edge`'s own env should hold for the usage tail Worker
|
|
1443
|
+
* (`USAGE_TAIL_WORKER`, `EDGE_USAGE_KEY_HASH`), or `null` while the Worker is
|
|
1444
|
+
* not deployed or its key is not held yet. `provisionPlatformEdge` writes
|
|
1445
|
+
* these two names on every run alongside `EDGE_PLATFORM_KEY_HASH`; `planEdge`
|
|
1446
|
+
* reads {@link usageTailEdgeEnvValue} to replan edge once this changes.
|
|
1447
|
+
*/
|
|
1448
|
+
export function usageTailEdgeEnv(recordedIds) {
|
|
1449
|
+
const deployed = Boolean(recordedIds[resourceKey('cloudflare', 'worker', PLATFORM_USAGE_TAIL_KEY)]) &&
|
|
1450
|
+
!recordedIds[PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY];
|
|
1451
|
+
const hash = recordedIds[PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY];
|
|
1452
|
+
if (!deployed || !hash)
|
|
1453
|
+
return null;
|
|
1454
|
+
return { USAGE_TAIL_WORKER: PLATFORM_USAGE_TAIL_SCRIPT_NAME, EDGE_USAGE_KEY_HASH: hash };
|
|
1455
|
+
}
|
|
1456
|
+
/** The value `provisionPlatformEdge` records once it has written {@link usageTailEdgeEnv} to edge's env; empty while there is nothing to write. */
|
|
1457
|
+
export function usageTailEdgeEnvValue(recordedIds) {
|
|
1458
|
+
const env = usageTailEdgeEnv(recordedIds);
|
|
1459
|
+
return env ? `${env.USAGE_TAIL_WORKER}|${env.EDGE_USAGE_KEY_HASH}` : '';
|
|
1460
|
+
}
|
|
1461
|
+
/** Whether anything of the usage tail Worker is recorded that disabling it must remove. */
|
|
1462
|
+
export function usageTailRecorded(recordedIds) {
|
|
1463
|
+
return ((Boolean(recordedIds[resourceKey('cloudflare', 'worker', PLATFORM_USAGE_TAIL_KEY)]) &&
|
|
1464
|
+
!recordedIds[PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY]) ||
|
|
1465
|
+
Boolean(recordedIds[PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY]));
|
|
1466
|
+
}
|
|
1399
1467
|
/**
|
|
1400
1468
|
* #167: the platform's own diff engine, mirroring `diff.ts`'s
|
|
1401
1469
|
* `computePlanDetail` for the one shared {@link DeclaredPlatform} record
|
|
@@ -1449,6 +1517,11 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1449
1517
|
// write, so while edge is neither applied nor planned in this plan they are
|
|
1450
1518
|
// blocked, not planned (rule 2), naming edge as the cause.
|
|
1451
1519
|
const edgeApplied = Boolean(recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]);
|
|
1520
|
+
// foundry#1024: after edge, which it uploads the script through and whose
|
|
1521
|
+
// env takes the key's hash.
|
|
1522
|
+
const usageTail = planUsageTail(platform, recordedIds, blocked.usageTail, edgeApplied);
|
|
1523
|
+
if (usageTail.step)
|
|
1524
|
+
plannedSteps.push(usageTail.step);
|
|
1452
1525
|
const dnsBlocked = edgeApplied || edge.step
|
|
1453
1526
|
? undefined
|
|
1454
1527
|
: (service) => `the platform's ${service} is blocked, not planned: every DNS record is written through edge, and edge is neither applied nor planned (${platform.edge.enabled
|
|
@@ -1480,7 +1553,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1480
1553
|
// reconcile below can be planned in the *same* round hc first comes up,
|
|
1481
1554
|
// the same way files/mail/archon are planned in auth's own first round.
|
|
1482
1555
|
const hcReady = hc.step !== null || Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_HC_KEY)]);
|
|
1483
|
-
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false);
|
|
1556
|
+
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false, blocked.authMailKeyHeld ?? true);
|
|
1484
1557
|
if (auth.step)
|
|
1485
1558
|
plannedSteps.push(auth.step);
|
|
1486
1559
|
// #174: the integrations host. Planned after auth; neither step reads the
|
|
@@ -1718,6 +1791,9 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1718
1791
|
unmanaged.push({ description: heartbeats.blocked, key: 'heartbeats' });
|
|
1719
1792
|
for (const description of uptime.blocked)
|
|
1720
1793
|
unmanaged.push({ description, key: 'uptime' });
|
|
1794
|
+
for (const description of usageTail.blocked) {
|
|
1795
|
+
unmanaged.push({ description, key: 'usage-tail' });
|
|
1796
|
+
}
|
|
1721
1797
|
if (releaseHook.blocked) {
|
|
1722
1798
|
unmanaged.push({ description: releaseHook.blocked, key: 'github-release-hook' });
|
|
1723
1799
|
}
|
|
@@ -2153,7 +2229,7 @@ function planSfu(platform, callsOrgSlugs, recordedIds) {
|
|
|
2153
2229
|
* promising "this apply re-mints" while one of those is still missing would
|
|
2154
2230
|
* be wrong the moment it is read.
|
|
2155
2231
|
*/
|
|
2156
|
-
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false) {
|
|
2232
|
+
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false, mailKeyHeld = true) {
|
|
2157
2233
|
if (!platform.auth.enabled)
|
|
2158
2234
|
return { step: null, blocked: null };
|
|
2159
2235
|
// C3/S2: standing notes. The invite and the domain wait on a person; they hold nothing else back.
|
|
@@ -2211,6 +2287,11 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
2211
2287
|
// step's redeploy, so a platform recorded before them still plans it.
|
|
2212
2288
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
2213
2289
|
recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
|
|
2290
|
+
// The login apps' mail env: a platform from before it, or with a new apex, is written again.
|
|
2291
|
+
// Without the held key the apply cannot record it, so it is asked for only once the key is held.
|
|
2292
|
+
(!mailKeyHeld ||
|
|
2293
|
+
recordedIds[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] ===
|
|
2294
|
+
authLoginEnvMarker(`noreply@${platform.apex}`)) &&
|
|
2214
2295
|
recordedIds[PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY] === platform.auth.gitRef) {
|
|
2215
2296
|
return { step: null, blocked: standingNote };
|
|
2216
2297
|
}
|
|
@@ -2686,7 +2767,10 @@ function planFiles(platform, recordedIds, executorBlocked, authReady, drifted) {
|
|
|
2686
2767
|
*
|
|
2687
2768
|
* `drifted` (the host's own `driftedSteps`: edge holds an older Cloudflare
|
|
2688
2769
|
* token) plans the same step on an applied edge. The step writes the held
|
|
2689
|
-
* token into edge's env and redeploys it.
|
|
2770
|
+
* token into edge's env and redeploys it. foundry#1024: a usage tail Worker
|
|
2771
|
+
* key minted after edge's last deploy plans the step again the same way,
|
|
2772
|
+
* through {@link usageTailEdgeEnvValue} rather than a separate `drifted`
|
|
2773
|
+
* flag, since the planner can read it straight from `recordedIds`.
|
|
2690
2774
|
*
|
|
2691
2775
|
* Blocked, not planned: a platform key minted but never delivered (lost,
|
|
2692
2776
|
* `ServiceError`-free the way {@link planFiles}'s own management-key check
|
|
@@ -2708,7 +2792,11 @@ function planEdge(platform, recordedIds, executorBlocked, drifted) {
|
|
|
2708
2792
|
recordedIds[PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY] === platform.edge.hostname &&
|
|
2709
2793
|
recordedIds[PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY] === platform.edge.gitRef &&
|
|
2710
2794
|
// F4.dns: an edge applied before its address was recorded runs once more to record it.
|
|
2711
|
-
Boolean(recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY])
|
|
2795
|
+
Boolean(recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]) &&
|
|
2796
|
+
// foundry#1024: a usage tail Worker key minted after edge's last deploy
|
|
2797
|
+
// has not reached edge's env yet; replan to write it and redeploy.
|
|
2798
|
+
(recordedIds[PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY] ?? '') ===
|
|
2799
|
+
usageTailEdgeEnvValue(recordedIds);
|
|
2712
2800
|
if (applied && !drifted)
|
|
2713
2801
|
return { step: null, blocked: null };
|
|
2714
2802
|
if (executorBlocked)
|
|
@@ -3303,6 +3391,79 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
|
|
|
3303
3391
|
notes,
|
|
3304
3392
|
};
|
|
3305
3393
|
}
|
|
3394
|
+
/**
|
|
3395
|
+
* foundry#1024: the usage tail Worker, one `cloudflare:worker` step. Blocked,
|
|
3396
|
+
* not planned (rule 2), while no artifact is pinned, edge is not applied, or
|
|
3397
|
+
* the executor lacks its dependencies. Planned again when the pinned tag or
|
|
3398
|
+
* sha256 changed, or a half-made deploy never recorded its applied values; it
|
|
3399
|
+
* mints nothing that is already held. When the platform no longer enables
|
|
3400
|
+
* the Worker, a destructive step removes whatever of it is recorded.
|
|
3401
|
+
*/
|
|
3402
|
+
function planUsageTail(platform, recordedIds, executorBlocked, edgeApplied) {
|
|
3403
|
+
const { usageTail } = platform;
|
|
3404
|
+
const deployed = Boolean(recordedIds[resourceKey('cloudflare', 'worker', PLATFORM_USAGE_TAIL_KEY)]) &&
|
|
3405
|
+
!recordedIds[PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY];
|
|
3406
|
+
const action = (desired) => ({
|
|
3407
|
+
system: 'cloudflare',
|
|
3408
|
+
kind: 'worker',
|
|
3409
|
+
key: PLATFORM_USAGE_TAIL_KEY,
|
|
3410
|
+
desired,
|
|
3411
|
+
});
|
|
3412
|
+
if (!usageTail.enabled) {
|
|
3413
|
+
if (!usageTailRecorded(recordedIds))
|
|
3414
|
+
return { step: null, blocked: [] };
|
|
3415
|
+
if (executorBlocked)
|
|
3416
|
+
return { step: null, blocked: [executorBlocked] };
|
|
3417
|
+
return {
|
|
3418
|
+
step: {
|
|
3419
|
+
action: action({ remove: true }),
|
|
3420
|
+
step: {
|
|
3421
|
+
description: `remove the usage tail Worker ${PLATFORM_USAGE_TAIL_SCRIPT_NAME} and its held key`,
|
|
3422
|
+
destroys: true,
|
|
3423
|
+
migrates: false,
|
|
3424
|
+
},
|
|
3425
|
+
},
|
|
3426
|
+
blocked: [],
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
if (!usageTail.tag || !usageTail.sha256) {
|
|
3430
|
+
return {
|
|
3431
|
+
step: null,
|
|
3432
|
+
blocked: [
|
|
3433
|
+
'the usage tail Worker is blocked, not planned: no artifact is pinned; declare usageTail.tag (an edge release tag) and usageTail.sha256 (the digest of usage-tail-worker.js at that tag)',
|
|
3434
|
+
],
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
if (!edgeApplied) {
|
|
3438
|
+
return {
|
|
3439
|
+
step: null,
|
|
3440
|
+
blocked: [
|
|
3441
|
+
'the usage tail Worker is blocked, not planned: platform:edge has not been applied yet',
|
|
3442
|
+
],
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
if (executorBlocked)
|
|
3446
|
+
return { step: null, blocked: [executorBlocked] };
|
|
3447
|
+
// Applied only once a run got to its end and recorded these: a Worker that
|
|
3448
|
+
// was uploaded and then failed has its key but none of the applied values.
|
|
3449
|
+
const applied = deployed &&
|
|
3450
|
+
Boolean(recordedIds[PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY]) &&
|
|
3451
|
+
recordedIds[PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY] === usageTail.tag &&
|
|
3452
|
+
recordedIds[PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY] === usageTail.sha256;
|
|
3453
|
+
if (applied)
|
|
3454
|
+
return { step: null, blocked: [] };
|
|
3455
|
+
return {
|
|
3456
|
+
step: {
|
|
3457
|
+
action: action({ gitRef: usageTail.tag, sha256: usageTail.sha256 }),
|
|
3458
|
+
step: {
|
|
3459
|
+
description: `deploy the usage tail Worker ${PLATFORM_USAGE_TAIL_SCRIPT_NAME} from edge release ${usageTail.tag}, with its held key`,
|
|
3460
|
+
destroys: false,
|
|
3461
|
+
migrates: false,
|
|
3462
|
+
},
|
|
3463
|
+
},
|
|
3464
|
+
blocked: [],
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3306
3467
|
/**
|
|
3307
3468
|
* #172: the platform's AI host (https://github.com/wtfalch/ai) -- a Coolify
|
|
3308
3469
|
* Postgres plus three apps built from that repo's root Dockerfile, one per
|
|
@@ -28,6 +28,7 @@ export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
|
28
28
|
import { type PlatformGithubReleaseHookDeps } from './github-release-hook.js';
|
|
29
29
|
import { type CallsSfuPlatformDeps } from './platform-sfu.js';
|
|
30
30
|
import { type PlatformUptimeDeps } from './uptime-worker.js';
|
|
31
|
+
import { type PlatformUsageTailDeps } from './usage-tail-worker.js';
|
|
31
32
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
32
33
|
export declare function apexZoneKey(apex: string): string;
|
|
33
34
|
/**
|
|
@@ -326,6 +327,12 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
326
327
|
* the same reason as `auth`; without it the step is reported blocked.
|
|
327
328
|
*/
|
|
328
329
|
uptime?: PlatformUptimeDeps;
|
|
330
|
+
/**
|
|
331
|
+
* foundry#1024: dependencies for the usage tail Worker step
|
|
332
|
+
* (`cloudflare:worker` keyed {@link PLATFORM_USAGE_TAIL_KEY}) -- optional
|
|
333
|
+
* for the same reason as `auth`; without it the step is reported blocked.
|
|
334
|
+
*/
|
|
335
|
+
usageTail?: PlatformUsageTailDeps;
|
|
329
336
|
/**
|
|
330
337
|
* #604: dependencies for the `cloudflare:access-provider` step -- optional
|
|
331
338
|
* for the same reason as `auth`; edge and sign-in applied with this unset
|
|
@@ -362,6 +369,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
362
369
|
private readonly registry?;
|
|
363
370
|
private readonly heartbeats?;
|
|
364
371
|
private readonly uptime?;
|
|
372
|
+
private readonly usageTail?;
|
|
365
373
|
private readonly accessProvider?;
|
|
366
374
|
private readonly oikosIngress?;
|
|
367
375
|
private readonly githubReleaseHook?;
|
|
@@ -401,6 +409,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
401
409
|
auth?: string;
|
|
402
410
|
authExecutorMissing?: boolean;
|
|
403
411
|
authOtherInputsMissing?: string[];
|
|
412
|
+
authMailKeyHeld?: boolean;
|
|
404
413
|
authOwnerInvite?: string;
|
|
405
414
|
files?: string;
|
|
406
415
|
integrations?: string;
|
|
@@ -427,6 +436,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
427
436
|
pending: boolean;
|
|
428
437
|
blocked?: string;
|
|
429
438
|
};
|
|
439
|
+
usageTail?: string;
|
|
430
440
|
githubReleaseHook?: string;
|
|
431
441
|
githubReleaseHookReady?: boolean;
|
|
432
442
|
githubReleaseHookHash?: string;
|
|
@@ -449,6 +459,12 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
449
459
|
* they hold.
|
|
450
460
|
*/
|
|
451
461
|
private uptimeBlocked;
|
|
462
|
+
/**
|
|
463
|
+
* foundry#1024: why the usage tail Worker cannot run yet. A lost key is
|
|
464
|
+
* not a block here: the step mints a new one. The planner checks the pin
|
|
465
|
+
* and edge first, so none of the reads here happens before they hold.
|
|
466
|
+
*/
|
|
467
|
+
private usageTailBlocked;
|
|
452
468
|
/**
|
|
453
469
|
* Whether Archon's live Access policy lets through anyone but the declared
|
|
454
470
|
* emails, or (#211 round 4) the broker it last registered with no longer
|
|
@@ -33,7 +33,7 @@ import { MAIL_APP_SCOPE_KEY, MAIL_DATABASE_NAME, MAIL_DATABASE_PASSWORD_ENV, MAI
|
|
|
33
33
|
import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
|
|
34
34
|
import { recordOwnDnsHostname } from './own-dns.js';
|
|
35
35
|
import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
|
|
36
|
-
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, GITHUB_RELEASE_HOOK_DEPS_MISSING, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_GITHUB_RELEASE_HOOK_KEY, PLATFORM_HC_ACCESS_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_USER_ROLE, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeTicksSyncValue, uptimeVarsDigest, } from './platform-diff.js';
|
|
36
|
+
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, GITHUB_RELEASE_HOOK_DEPS_MISSING, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_GITHUB_RELEASE_HOOK_KEY, PLATFORM_HC_ACCESS_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_USER_ROLE, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_USAGE_TAIL_KEY, PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, PLATFORM_USAGE_TAIL_SCRIPT_NAME, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, authLoginEnvMarker, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeTicksSyncValue, uptimeVarsDigest, usageTailEdgeEnv, usageTailEdgeEnvValue, usageTailRecorded, } from './platform-diff.js';
|
|
37
37
|
import { PLATFORM_REGISTRY_KEY, PLATFORM_REGISTRY_LOGINS_KEY, PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY, PLATFORM_REGISTRY_PULL_PASSWORD_REF, registryImageTagBlocked, } from './platform-registry-keys.js';
|
|
38
38
|
import { ensureRegistryLogins, provisionPlatformRegistry, registryLoginsDrifted, } from './platform-registry.js';
|
|
39
39
|
import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
|
|
@@ -47,6 +47,7 @@ export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
|
47
47
|
import { githubReleaseHookPlanInputs, provisionGithubReleaseHook, } from './github-release-hook.js';
|
|
48
48
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
49
49
|
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
50
|
+
import { provisionPlatformUsageTail } from './usage-tail-worker.js';
|
|
50
51
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, waitForSystemApi, } from './zitadel-system-api.js';
|
|
51
52
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
52
53
|
export function apexZoneKey(apex) {
|
|
@@ -152,6 +153,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
152
153
|
registry;
|
|
153
154
|
heartbeats;
|
|
154
155
|
uptime;
|
|
156
|
+
usageTail;
|
|
155
157
|
accessProvider;
|
|
156
158
|
oikosIngress;
|
|
157
159
|
githubReleaseHook;
|
|
@@ -174,6 +176,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
174
176
|
this.registry = options.registry;
|
|
175
177
|
this.heartbeats = options.heartbeats;
|
|
176
178
|
this.uptime = options.uptime;
|
|
179
|
+
this.usageTail = options.usageTail;
|
|
177
180
|
this.accessProvider = options.accessProvider;
|
|
178
181
|
this.oikosIngress = options.oikosIngress;
|
|
179
182
|
this.githubReleaseHook = options.githubReleaseHook;
|
|
@@ -407,7 +410,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
407
410
|
blocked.authExecutorMissing = true;
|
|
408
411
|
}
|
|
409
412
|
else if (platform.auth.enabled && this.auth) {
|
|
410
|
-
const { reason: authBlocked, otherInputsMissing } = await authBrokerBlockedReason(this.auth, recordedIds);
|
|
413
|
+
const { reason: authBlocked, otherInputsMissing, mailKeyHeld, } = await authBrokerBlockedReason(this.auth, recordedIds);
|
|
411
414
|
if (authBlocked) {
|
|
412
415
|
blocked.auth = authBlocked;
|
|
413
416
|
}
|
|
@@ -424,6 +427,9 @@ export class FoundryPlatformStepExecutor {
|
|
|
424
427
|
// #216 round 5: see `PlanPlatformOptions.blocked.authOtherInputsMissing`.
|
|
425
428
|
if (otherInputsMissing.length > 0)
|
|
426
429
|
blocked.authOtherInputsMissing = otherInputsMissing;
|
|
430
|
+
// Without the held key `provisionAuthStack` never records the login env marker, so the plan must not ask for it.
|
|
431
|
+
if (mailKeyHeld !== undefined)
|
|
432
|
+
blocked.authMailKeyHeld = mailKeyHeld;
|
|
427
433
|
// C3: the owner's invite needs a work address and the host's mail port. Without them it is
|
|
428
434
|
// a blocked item, never a throw on every apply.
|
|
429
435
|
const ownerCompany = await this.declaredOrgStore.get(platform.ownerOrgId);
|
|
@@ -612,6 +618,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
612
618
|
blocked.heartbeatsApiKeyMissing = true;
|
|
613
619
|
}
|
|
614
620
|
await this.uptimeBlocked(platform, recordedIds, blocked, owner);
|
|
621
|
+
await this.usageTailBlocked(platform, recordedIds, blocked);
|
|
615
622
|
if (platform.archon.enabled && !this.githubReleaseHook) {
|
|
616
623
|
blocked.githubReleaseHook = GITHUB_RELEASE_HOOK_DEPS_MISSING;
|
|
617
624
|
}
|
|
@@ -797,6 +804,38 @@ export class FoundryPlatformStepExecutor {
|
|
|
797
804
|
}
|
|
798
805
|
}
|
|
799
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* foundry#1024: why the usage tail Worker cannot run yet. A lost key is
|
|
809
|
+
* not a block here: the step mints a new one. The planner checks the pin
|
|
810
|
+
* and edge first, so none of the reads here happens before they hold.
|
|
811
|
+
*/
|
|
812
|
+
async usageTailBlocked(platform, recordedIds, blocked) {
|
|
813
|
+
const deployed = Boolean(recordedIds[`cloudflare:worker:${PLATFORM_USAGE_TAIL_KEY}`]) &&
|
|
814
|
+
!recordedIds[PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY];
|
|
815
|
+
if (!platform.usageTail.enabled && !usageTailRecorded(recordedIds))
|
|
816
|
+
return;
|
|
817
|
+
const deps = this.usageTail;
|
|
818
|
+
if (!deps) {
|
|
819
|
+
blocked.usageTail =
|
|
820
|
+
'the usage tail Worker is blocked, not planned: the host built the platform executor without its usage-tail dependencies';
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (!platform.usageTail.enabled)
|
|
824
|
+
return;
|
|
825
|
+
const edgeUrl = recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
826
|
+
if (!deployed && edgeUrl && platform.usageTail.tag) {
|
|
827
|
+
try {
|
|
828
|
+
const workers = await deps.workers(edgeUrl);
|
|
829
|
+
if (await workers.scriptExists(PLATFORM_USAGE_TAIL_SCRIPT_NAME)) {
|
|
830
|
+
blocked.usageTail = `the usage tail Worker is blocked, not planned: a Worker named ${PLATFORM_USAGE_TAIL_SCRIPT_NAME} already exists at Cloudflare and foundry did not make it; remove it by hand`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
blocked.usageTail =
|
|
835
|
+
'the usage tail Worker is blocked, not planned: edge could not be asked whether the Worker exists';
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
800
839
|
/**
|
|
801
840
|
* Whether Archon's live Access policy lets through anyone but the declared
|
|
802
841
|
* emails, or (#211 round 4) the broker it last registered with no longer
|
|
@@ -1156,6 +1195,14 @@ export class FoundryPlatformStepExecutor {
|
|
|
1156
1195
|
}
|
|
1157
1196
|
return provisionPlatformUptime(context, this.uptime);
|
|
1158
1197
|
}
|
|
1198
|
+
if (action.system === 'cloudflare' &&
|
|
1199
|
+
action.kind === 'worker' &&
|
|
1200
|
+
action.key === PLATFORM_USAGE_TAIL_KEY) {
|
|
1201
|
+
if (!this.usageTail) {
|
|
1202
|
+
throw new Error('platform:usage-tail needs FoundryPlatformStepExecutor to be constructed with its usage-tail dependencies (foundry#1024)');
|
|
1203
|
+
}
|
|
1204
|
+
return provisionPlatformUsageTail(context, this.usageTail);
|
|
1205
|
+
}
|
|
1159
1206
|
if (action.system === 'coolify' &&
|
|
1160
1207
|
action.kind === 'app' &&
|
|
1161
1208
|
action.key === PLATFORM_OIKOS_INGRESS_KEY) {
|
|
@@ -2239,7 +2286,16 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2239
2286
|
await waitHealthy(withCallTimeout(deps.coolify), zitadelAppId, runtime, label, 'ZITADEL');
|
|
2240
2287
|
await record(SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, '');
|
|
2241
2288
|
}
|
|
2242
|
-
|
|
2289
|
+
// The mail env counts as live only once the login apps were deployed after it was written: a run that
|
|
2290
|
+
// died between the write and the deploy leaves the marker unset, and the next run sees no env diff.
|
|
2291
|
+
const loginEnvUnconfirmed = Boolean(heldMailKey &&
|
|
2292
|
+
input.smtpSender &&
|
|
2293
|
+
recorded[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] !== authLoginEnvMarker(input.smtpSender));
|
|
2294
|
+
if (loginApp.justCreated ||
|
|
2295
|
+
!loginApp.running ||
|
|
2296
|
+
loginEnvsChanged ||
|
|
2297
|
+
loginEnvUnconfirmed ||
|
|
2298
|
+
authRefChanged) {
|
|
2243
2299
|
await deps.coolify.deploy(loginAppId);
|
|
2244
2300
|
}
|
|
2245
2301
|
// The key counts as live only once the app was deployed after it was written: a crash between the write
|
|
@@ -2251,11 +2307,17 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2251
2307
|
signInEnvsChanged ||
|
|
2252
2308
|
bindKeyWritten ||
|
|
2253
2309
|
bindKeyUnconfirmed ||
|
|
2310
|
+
loginEnvUnconfirmed ||
|
|
2254
2311
|
authRefChanged)) {
|
|
2255
2312
|
await deps.coolify.deploy(signInApp.appId);
|
|
2256
2313
|
if (input.signInLogin)
|
|
2257
2314
|
await record(SIGN_IN_BIND_KEY_SCOPE_KEY, '1');
|
|
2258
2315
|
}
|
|
2316
|
+
// The mail env is live on the login apps: written above and deployed with them. Without the held
|
|
2317
|
+
// key the marker stays unset, so the step is planned again once the key is held.
|
|
2318
|
+
if (heldMailKey && input.smtpSender) {
|
|
2319
|
+
await record(PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, authLoginEnvMarker(input.smtpSender));
|
|
2320
|
+
}
|
|
2259
2321
|
// #211: the broker's own `server.ts` refuses to start without
|
|
2260
2322
|
// `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `LETTERMINT_API_TOKEN` --
|
|
2261
2323
|
// deploying it without them only produces a container that crash-loops
|
|
@@ -4022,8 +4084,9 @@ async function authBrokerBlockedReason(auth, recordedIds) {
|
|
|
4022
4084
|
// #210: and `LETTERMINT_API_TOKEN`, once held -- see this function's own
|
|
4023
4085
|
// doc comment.
|
|
4024
4086
|
const otherInputsMissing = missing.filter((m) => m !== 'ZITADEL_SERVICE_USER_TOKEN' && !(m === 'LETTERMINT_API_TOKEN' && heldMailKey));
|
|
4087
|
+
const mailKeyHeld = Boolean(heldMailKey);
|
|
4025
4088
|
if (missing.length === 0)
|
|
4026
|
-
return { reason: null, otherInputsMissing };
|
|
4089
|
+
return { reason: null, otherInputsMissing, mailKeyHeld };
|
|
4027
4090
|
// #210: say where the operator seals the Lettermint key, not only its env name.
|
|
4028
4091
|
const named = (names) => names.map((m) => m === 'LETTERMINT_API_TOKEN'
|
|
4029
4092
|
? `LETTERMINT_API_TOKEN (seal it under the held-key ref ${PLATFORM_AUTH_BROKER_MAIL_KEY_REF})`
|
|
@@ -4036,6 +4099,7 @@ async function authBrokerBlockedReason(auth, recordedIds) {
|
|
|
4036
4099
|
return {
|
|
4037
4100
|
reason: `the platform's sign-in broker ${brokerAppId} is blocked, not deployed: its AUTH_APPS is malformed${otherSuffix}`,
|
|
4038
4101
|
otherInputsMissing,
|
|
4102
|
+
mailKeyHeld,
|
|
4039
4103
|
};
|
|
4040
4104
|
}
|
|
4041
4105
|
// AUTH_APPS isn't gated on #209/#210 the way the two tokens are; only cite
|
|
@@ -4044,6 +4108,7 @@ async function authBrokerBlockedReason(auth, recordedIds) {
|
|
|
4044
4108
|
return {
|
|
4045
4109
|
reason: `the platform's sign-in broker is created but not deployed, blocked: it has no ${named(missing).join(' and no ')} yet${suffix}`,
|
|
4046
4110
|
otherInputsMissing,
|
|
4111
|
+
mailKeyHeld,
|
|
4047
4112
|
};
|
|
4048
4113
|
}
|
|
4049
4114
|
/** Polls until Coolify reports the mail app stopped, so a healthy answer read afterwards is the new deployment's. */
|
|
@@ -8119,6 +8184,12 @@ async function edgeBlockedReason(names) {
|
|
|
8119
8184
|
* - `EDGE_TRUST_CF_CONNECTING_IP` is always written `true` rather than
|
|
8120
8185
|
* left to the host's own default, since this step always puts the
|
|
8121
8186
|
* runner behind the Cloudflare tunnel it also creates.
|
|
8187
|
+
*
|
|
8188
|
+
* foundry#1024: `USAGE_TAIL_WORKER` and `EDGE_USAGE_KEY_HASH` ({@link usageTailEdgeEnv})
|
|
8189
|
+
* are written on every run too, once the usage tail Worker step has deployed
|
|
8190
|
+
* and holds its key; `planEdge` replans this step while the recorded
|
|
8191
|
+
* {@link PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY} differs from what is
|
|
8192
|
+
* now desired, the same way a changed hostname or `gitRef` does.
|
|
8122
8193
|
*/
|
|
8123
8194
|
async function provisionPlatformEdge(context, deps) {
|
|
8124
8195
|
const { platform } = context;
|
|
@@ -8228,6 +8299,10 @@ async function provisionPlatformEdge(context, deps) {
|
|
|
8228
8299
|
await record(PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, keyHash);
|
|
8229
8300
|
}
|
|
8230
8301
|
// ---- base env, written on every run --------------------------------------
|
|
8302
|
+
// foundry#1024: the usage tail Worker's name and the hash of its key, so
|
|
8303
|
+
// every company script can name it as its tail consumer and edge's ingest
|
|
8304
|
+
// route can accept the key. Absent while the Worker is not deployed yet.
|
|
8305
|
+
const usageTailEnv = usageTailEdgeEnv(recorded);
|
|
8231
8306
|
await deps.coolify.bulkUpsertEnvs(appId, [
|
|
8232
8307
|
{ key: 'DATABASE_URL_OWNER', value: db.internal_db_url },
|
|
8233
8308
|
{ key: 'DATABASE_URL', value: runtimeUrl },
|
|
@@ -8237,6 +8312,12 @@ async function provisionPlatformEdge(context, deps) {
|
|
|
8237
8312
|
{ key: 'EDGE_TRUST_CF_CONNECTING_IP', value: 'true' },
|
|
8238
8313
|
{ key: 'NODE_ENV', value: 'production' },
|
|
8239
8314
|
{ key: 'EDGE_PLATFORM_KEY_HASH', value: keyHash },
|
|
8315
|
+
...(usageTailEnv
|
|
8316
|
+
? [
|
|
8317
|
+
{ key: 'USAGE_TAIL_WORKER', value: usageTailEnv.USAGE_TAIL_WORKER },
|
|
8318
|
+
{ key: 'EDGE_USAGE_KEY_HASH', value: usageTailEnv.EDGE_USAGE_KEY_HASH },
|
|
8319
|
+
]
|
|
8320
|
+
: []),
|
|
8240
8321
|
...(await missingEnvs(deps.coolify, appId, PLATFORM_EDGE_RUNNER_DEFAULTS)),
|
|
8241
8322
|
]);
|
|
8242
8323
|
// ---- deploy and wait, on edge's own address ---------------------------------
|
|
@@ -8297,6 +8378,7 @@ async function provisionPlatformEdge(context, deps) {
|
|
|
8297
8378
|
}
|
|
8298
8379
|
// Last: a run that failed anywhere above is planned again.
|
|
8299
8380
|
context.recordScopeId(PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, hostname);
|
|
8381
|
+
context.recordScopeId(PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY, usageTailEdgeEnvValue(recorded));
|
|
8300
8382
|
await record(PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, gitRef);
|
|
8301
8383
|
return { resourceId: appId };
|
|
8302
8384
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PlatformStepExecutionContext } from './plan-apply.js';
|
|
2
|
+
import type { PlatformWorkersFactory } from './uptime-worker.js';
|
|
3
|
+
/**
|
|
4
|
+
* Dependencies of the usage tail Worker step. `deliverSecret` and
|
|
5
|
+
* `heldKeys` are the host's: the minted key goes only to `deliverSecret`,
|
|
6
|
+
* and it is read back only to hand it to edge, which never returns one.
|
|
7
|
+
*/
|
|
8
|
+
export interface PlatformUsageTailDeps {
|
|
9
|
+
workers: PlatformWorkersFactory;
|
|
10
|
+
deliverSecret: (ref: string, secret: string) => Promise<void>;
|
|
11
|
+
heldKeys: {
|
|
12
|
+
resolve(ref: string): Promise<string | null>;
|
|
13
|
+
};
|
|
14
|
+
/** Tries per key delivery (rule 1), 3 by default. */
|
|
15
|
+
deliverAttempts?: number;
|
|
16
|
+
/** Waits between delivery tries; a real timer by default. */
|
|
17
|
+
sleep?: (ms: number) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* foundry#1024: makes the usage tail Worker true. Upload the pinned edge
|
|
21
|
+
* release (the host fetches and verifies it) and record the script at once,
|
|
22
|
+
* so a later failure is a half-made Worker foundry owns. Then hold the key
|
|
23
|
+
* (minted once, its guard recorded as soon as its value is held) and set it
|
|
24
|
+
* on the Worker. The applied values are recorded only when all of it
|
|
25
|
+
* succeeded.
|
|
26
|
+
*/
|
|
27
|
+
export declare function provisionPlatformUsageTail(context: PlatformStepExecutionContext, deps: PlatformUsageTailDeps): Promise<{
|
|
28
|
+
resourceId?: string;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { usageTailArtifactUrl } from '../platform.js';
|
|
3
|
+
import { PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY, PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY, PLATFORM_USAGE_TAIL_KEY, PLATFORM_USAGE_TAIL_KEY_REF, PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY, PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, PLATFORM_USAGE_TAIL_SCRIPT_NAME, } from './platform-diff.js';
|
|
4
|
+
/** The date Cloudflare's Workers runtime compatibility is pinned to; no Workers feature here needs a later one. */
|
|
5
|
+
const USAGE_TAIL_COMPATIBILITY_DATE = '2026-10-01';
|
|
6
|
+
/**
|
|
7
|
+
* foundry#1024: makes the usage tail Worker true. Upload the pinned edge
|
|
8
|
+
* release (the host fetches and verifies it) and record the script at once,
|
|
9
|
+
* so a later failure is a half-made Worker foundry owns. Then hold the key
|
|
10
|
+
* (minted once, its guard recorded as soon as its value is held) and set it
|
|
11
|
+
* on the Worker. The applied values are recorded only when all of it
|
|
12
|
+
* succeeded.
|
|
13
|
+
*/
|
|
14
|
+
export async function provisionPlatformUsageTail(context, deps) {
|
|
15
|
+
const { platform } = context;
|
|
16
|
+
const { usageTail } = platform;
|
|
17
|
+
const recorded = context.recordedIds();
|
|
18
|
+
const edgeUrl = recorded[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
19
|
+
if (!edgeUrl) {
|
|
20
|
+
throw new Error(`platform:usage-tail: edge has not been applied yet (no recorded ${PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY}); the Worker is uploaded through it`);
|
|
21
|
+
}
|
|
22
|
+
const workers = await deps.workers(edgeUrl);
|
|
23
|
+
if (!usageTail.enabled) {
|
|
24
|
+
// Whatever of a (possibly half-made) deploy exists is removed; what does not exist is tolerated.
|
|
25
|
+
await workers.deleteScript(PLATFORM_USAGE_TAIL_SCRIPT_NAME);
|
|
26
|
+
await deps.deliverSecret(PLATFORM_USAGE_TAIL_KEY_REF, '');
|
|
27
|
+
for (const key of [
|
|
28
|
+
PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY,
|
|
29
|
+
PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY,
|
|
30
|
+
PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY,
|
|
31
|
+
PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY,
|
|
32
|
+
]) {
|
|
33
|
+
context.recordScopeId(key, '');
|
|
34
|
+
}
|
|
35
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, '1');
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
if (!usageTail.tag || !usageTail.sha256) {
|
|
39
|
+
throw new Error('platform:usage-tail: no artifact is pinned (usageTail.tag, usageTail.sha256)');
|
|
40
|
+
}
|
|
41
|
+
// ---- the key, once ------------------------------------------------------
|
|
42
|
+
// The hash is never secret, so (the same pattern `provisionPlatformEdge`
|
|
43
|
+
// uses for `EDGE_PLATFORM_KEY_HASH`) it doubles as both the undelivered
|
|
44
|
+
// marker and the final recorded value: a marker with no guard means the
|
|
45
|
+
// mint may never have reached `deliverSecret`, so it is tried again with
|
|
46
|
+
// the same hash rather than minting a second key.
|
|
47
|
+
let keyHash = recorded[PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY] ?? '';
|
|
48
|
+
if (!keyHash) {
|
|
49
|
+
keyHash = recorded[PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY] ?? '';
|
|
50
|
+
let secret = keyHash ? await deps.heldKeys.resolve(PLATFORM_USAGE_TAIL_KEY_REF) : null;
|
|
51
|
+
if (!secret) {
|
|
52
|
+
secret = randomBytes(32).toString('base64url');
|
|
53
|
+
keyHash = createHash('sha256').update(secret).digest('hex');
|
|
54
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY, keyHash);
|
|
55
|
+
await context.persist?.();
|
|
56
|
+
}
|
|
57
|
+
await deliverKey(deps, secret);
|
|
58
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY, keyHash);
|
|
59
|
+
await context.persist?.();
|
|
60
|
+
}
|
|
61
|
+
// ---- the script ----------------------------------------------------------
|
|
62
|
+
await workers.uploadScript({
|
|
63
|
+
name: PLATFORM_USAGE_TAIL_SCRIPT_NAME,
|
|
64
|
+
artifactUrl: usageTailArtifactUrl(usageTail.tag),
|
|
65
|
+
artifactSha256: usageTail.sha256,
|
|
66
|
+
compatibilityDate: USAGE_TAIL_COMPATIBILITY_DATE,
|
|
67
|
+
bindings: [{ type: 'plain_text', name: 'EDGE_USAGE_URL', text: edgeUrl }],
|
|
68
|
+
});
|
|
69
|
+
// The script is foundry's from here on. Recorded now, not at the end: after
|
|
70
|
+
// a later failure the next plan must repair this Worker, not take it for a
|
|
71
|
+
// foreign one. "Fully applied" is the applied values recorded at the end.
|
|
72
|
+
context.recordScopeId(`cloudflare:worker:${PLATFORM_USAGE_TAIL_KEY}`, PLATFORM_USAGE_TAIL_SCRIPT_NAME);
|
|
73
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, '');
|
|
74
|
+
await context.persist?.();
|
|
75
|
+
// The key itself is read back only to hand it to the Worker (rule 6: it
|
|
76
|
+
// never reaches an error, a log, or a plan item); edge never returns it.
|
|
77
|
+
const secretValue = await deps.heldKeys.resolve(PLATFORM_USAGE_TAIL_KEY_REF);
|
|
78
|
+
if (!secretValue) {
|
|
79
|
+
throw new Error(`platform:usage-tail: the held key (${PLATFORM_USAGE_TAIL_KEY_REF}) is gone after it was minted; hold a new one to recover`);
|
|
80
|
+
}
|
|
81
|
+
await workers.setSecrets(PLATFORM_USAGE_TAIL_SCRIPT_NAME, [
|
|
82
|
+
{ name: 'EDGE_USAGE_KEY', value: secretValue, version: keyHash },
|
|
83
|
+
]);
|
|
84
|
+
// Last, after every write the applied values depend on.
|
|
85
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY, usageTail.tag);
|
|
86
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY, usageTail.sha256);
|
|
87
|
+
return { resourceId: PLATFORM_USAGE_TAIL_SCRIPT_NAME };
|
|
88
|
+
}
|
|
89
|
+
/** Rule 1: delivery is tried up to `deliverAttempts` times; the error names the ref only, never the cause. */
|
|
90
|
+
async function deliverKey(deps, secret) {
|
|
91
|
+
const attempts = deps.deliverAttempts ?? 3;
|
|
92
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
93
|
+
for (let attempt = 1;; attempt += 1) {
|
|
94
|
+
try {
|
|
95
|
+
await deps.deliverSecret(PLATFORM_USAGE_TAIL_KEY_REF, secret);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
if (attempt >= attempts) {
|
|
100
|
+
throw new Error(`platform:usage-tail: the key (${PLATFORM_USAGE_TAIL_KEY_REF}) could not be delivered after ${attempts} tries; it went nowhere, so the next apply mints a new one`);
|
|
101
|
+
}
|
|
102
|
+
await sleep(1000 * attempt);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/platform.d.ts
CHANGED
|
@@ -66,6 +66,24 @@ export interface PlatformUptimeWorker {
|
|
|
66
66
|
}
|
|
67
67
|
/** Where Archon publishes the Worker script for `tag` (a private repo; the host's `fetchArtifact` fetches it). */
|
|
68
68
|
export declare function uptimeArtifactUrl(tag: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* foundry#1024: the usage tail Worker that meters every company Worker's
|
|
71
|
+
* use. `@wtfalch/edge`'s `packages/service/src/usage-tail-worker.ts` has no
|
|
72
|
+
* imports and builds to `usage-tail-worker.js`; foundry deploys the pinned
|
|
73
|
+
* build and never builds it, the same `tag`/`sha256` pin
|
|
74
|
+
* {@link PlatformUptimeWorker} uses. Both empty means nothing is pinned yet,
|
|
75
|
+
* and the planner reports that instead of planning the step. It has no
|
|
76
|
+
* hostname: it answers no request, only the tail events Cloudflare delivers.
|
|
77
|
+
*/
|
|
78
|
+
export interface PlatformUsageTailWorker {
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
/** An edge release tag, `v1.2.3`. */
|
|
81
|
+
tag: string;
|
|
82
|
+
/** Lower-case hex SHA-256 of `usage-tail-worker.js` at that tag. */
|
|
83
|
+
sha256: string;
|
|
84
|
+
}
|
|
85
|
+
/** Where edge publishes the Worker script for `tag` (the host's `fetchArtifact` fetches it). */
|
|
86
|
+
export declare function usageTailArtifactUrl(tag: string): string;
|
|
69
87
|
/**
|
|
70
88
|
* The shared services a declared platform names, decided in #165, plus
|
|
71
89
|
* `edge` (#317, F7): the service that mints an entry's own edge key and
|
|
@@ -137,6 +155,8 @@ export interface DeclaredPlatform {
|
|
|
137
155
|
coolify: PlatformService;
|
|
138
156
|
/** foundry#542: the outside prober Worker; see {@link PlatformUptimeWorker}. */
|
|
139
157
|
uptime: PlatformUptimeWorker;
|
|
158
|
+
/** foundry#1024: the usage tail Worker; see {@link PlatformUsageTailWorker}. */
|
|
159
|
+
usageTail: PlatformUsageTailWorker;
|
|
140
160
|
/**
|
|
141
161
|
* foundry#544: which Healthchecks instance manages the heartbeat checks
|
|
142
162
|
* for every piece of scheduled work foundry sets up (the uptime Worker,
|
package/dist/platform.js
CHANGED
|
@@ -4,6 +4,10 @@ import { validateBlueGreen, validateDomain } from './org.js';
|
|
|
4
4
|
export function uptimeArtifactUrl(tag) {
|
|
5
5
|
return `https://github.com/wtfalch/archon/releases/download/${tag}/archon-uptime-worker.js`;
|
|
6
6
|
}
|
|
7
|
+
/** Where edge publishes the Worker script for `tag` (the host's `fetchArtifact` fetches it). */
|
|
8
|
+
export function usageTailArtifactUrl(tag) {
|
|
9
|
+
return `https://github.com/wtfalch/edge/releases/download/${tag}/usage-tail-worker.js`;
|
|
10
|
+
}
|
|
7
11
|
export const PLATFORM_SERVICE_KEYS = [
|
|
8
12
|
'auth',
|
|
9
13
|
'mail',
|
|
@@ -87,6 +91,16 @@ export function validatePlatform(platform) {
|
|
|
87
91
|
if (uptime.sha256 && !/^[0-9a-f]{64}$/.test(uptime.sha256)) {
|
|
88
92
|
return 'uptime.sha256 must be 64 lower-case hex characters';
|
|
89
93
|
}
|
|
94
|
+
const { usageTail } = platform;
|
|
95
|
+
if (Boolean(usageTail.tag.trim()) !== Boolean(usageTail.sha256.trim())) {
|
|
96
|
+
return 'usageTail.tag and usageTail.sha256 must be set together: a pin is both';
|
|
97
|
+
}
|
|
98
|
+
if (usageTail.tag && !/^v[0-9A-Za-z][0-9A-Za-z.+_-]*$/.test(usageTail.tag)) {
|
|
99
|
+
return `usageTail.tag '${usageTail.tag}' must be a release tag such as v1.2.3`;
|
|
100
|
+
}
|
|
101
|
+
if (usageTail.sha256 && !/^[0-9a-f]{64}$/.test(usageTail.sha256)) {
|
|
102
|
+
return 'usageTail.sha256 must be 64 lower-case hex characters';
|
|
103
|
+
}
|
|
90
104
|
// #491: declared, never discovered -- a present-but-blank id is refused the
|
|
91
105
|
// same way `serverIpv4`'s blank string is, rather than being read as unset.
|
|
92
106
|
if (platform.mail.lettermintProjectId != null && !platform.mail.lettermintProjectId.trim()) {
|
package/dist/store/postgres.js
CHANGED
|
@@ -815,6 +815,11 @@ function rowToPlatform(row) {
|
|
|
815
815
|
gitRef: row.coolifyGitRef,
|
|
816
816
|
},
|
|
817
817
|
uptime: { enabled: row.uptimeEnabled, tag: row.uptimeTag, sha256: row.uptimeSha256 },
|
|
818
|
+
usageTail: {
|
|
819
|
+
enabled: row.usageTailEnabled,
|
|
820
|
+
tag: row.usageTailTag,
|
|
821
|
+
sha256: row.usageTailSha256,
|
|
822
|
+
},
|
|
818
823
|
heartbeatsProvider: row.heartbeatsProvider,
|
|
819
824
|
};
|
|
820
825
|
}
|
|
@@ -867,6 +872,9 @@ function platformToRow(platform) {
|
|
|
867
872
|
uptimeEnabled: platform.uptime.enabled,
|
|
868
873
|
uptimeTag: platform.uptime.tag,
|
|
869
874
|
uptimeSha256: platform.uptime.sha256,
|
|
875
|
+
usageTailEnabled: platform.usageTail.enabled,
|
|
876
|
+
usageTailTag: platform.usageTail.tag,
|
|
877
|
+
usageTailSha256: platform.usageTail.sha256,
|
|
870
878
|
heartbeatsProvider: platform.heartbeatsProvider,
|
|
871
879
|
};
|
|
872
880
|
}
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -2445,6 +2445,57 @@ export declare const foundryDeclaredPlatform: import("drizzle-orm/pg-core").PgTa
|
|
|
2445
2445
|
identity: undefined;
|
|
2446
2446
|
generated: undefined;
|
|
2447
2447
|
}, {}, {}>;
|
|
2448
|
+
usageTailEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
2449
|
+
name: "usage_tail_enabled";
|
|
2450
|
+
tableName: "foundry_declared_platform";
|
|
2451
|
+
dataType: "boolean";
|
|
2452
|
+
columnType: "PgBoolean";
|
|
2453
|
+
data: boolean;
|
|
2454
|
+
driverParam: boolean;
|
|
2455
|
+
notNull: true;
|
|
2456
|
+
hasDefault: true;
|
|
2457
|
+
isPrimaryKey: false;
|
|
2458
|
+
isAutoincrement: false;
|
|
2459
|
+
hasRuntimeDefault: false;
|
|
2460
|
+
enumValues: undefined;
|
|
2461
|
+
baseColumn: never;
|
|
2462
|
+
identity: undefined;
|
|
2463
|
+
generated: undefined;
|
|
2464
|
+
}, {}, {}>;
|
|
2465
|
+
usageTailTag: import("drizzle-orm/pg-core").PgColumn<{
|
|
2466
|
+
name: "usage_tail_tag";
|
|
2467
|
+
tableName: "foundry_declared_platform";
|
|
2468
|
+
dataType: "string";
|
|
2469
|
+
columnType: "PgText";
|
|
2470
|
+
data: string;
|
|
2471
|
+
driverParam: string;
|
|
2472
|
+
notNull: true;
|
|
2473
|
+
hasDefault: true;
|
|
2474
|
+
isPrimaryKey: false;
|
|
2475
|
+
isAutoincrement: false;
|
|
2476
|
+
hasRuntimeDefault: false;
|
|
2477
|
+
enumValues: [string, ...string[]];
|
|
2478
|
+
baseColumn: never;
|
|
2479
|
+
identity: undefined;
|
|
2480
|
+
generated: undefined;
|
|
2481
|
+
}, {}, {}>;
|
|
2482
|
+
usageTailSha256: import("drizzle-orm/pg-core").PgColumn<{
|
|
2483
|
+
name: "usage_tail_sha256";
|
|
2484
|
+
tableName: "foundry_declared_platform";
|
|
2485
|
+
dataType: "string";
|
|
2486
|
+
columnType: "PgText";
|
|
2487
|
+
data: string;
|
|
2488
|
+
driverParam: string;
|
|
2489
|
+
notNull: true;
|
|
2490
|
+
hasDefault: true;
|
|
2491
|
+
isPrimaryKey: false;
|
|
2492
|
+
isAutoincrement: false;
|
|
2493
|
+
hasRuntimeDefault: false;
|
|
2494
|
+
enumValues: [string, ...string[]];
|
|
2495
|
+
baseColumn: never;
|
|
2496
|
+
identity: undefined;
|
|
2497
|
+
generated: undefined;
|
|
2498
|
+
}, {}, {}>;
|
|
2448
2499
|
heartbeatsProvider: import("drizzle-orm/pg-core").PgColumn<{
|
|
2449
2500
|
name: "heartbeats_provider";
|
|
2450
2501
|
tableName: "foundry_declared_platform";
|
package/dist/store/schema.js
CHANGED
|
@@ -231,6 +231,10 @@ export const foundryDeclaredPlatform = pgTable('foundry_declared_platform', {
|
|
|
231
231
|
uptimeEnabled: boolean('uptime_enabled').notNull().default(true),
|
|
232
232
|
uptimeTag: text('uptime_tag').notNull().default(''),
|
|
233
233
|
uptimeSha256: text('uptime_sha256').notNull().default(''),
|
|
234
|
+
/** foundry#1024, `migrations/0070_platform_usage_tail.sql`: the usage tail Worker and its artifact pin. */
|
|
235
|
+
usageTailEnabled: boolean('usage_tail_enabled').notNull().default(false),
|
|
236
|
+
usageTailTag: text('usage_tail_tag').notNull().default(''),
|
|
237
|
+
usageTailSha256: text('usage_tail_sha256').notNull().default(''),
|
|
234
238
|
/** foundry#544, `migrations/0053_heartbeats_provider.sql`: which Healthchecks instance manages the heartbeat checks, decoupled from `hcEnabled` above. */
|
|
235
239
|
heartbeatsProvider: text('heartbeats_provider').notNull().default('self-hosted'),
|
|
236
240
|
/** `migrations/0018_platform_apply.sql`: the platform's applied state, `system:kind:key` to provider id. */
|