@wtfalch/foundry 0.69.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/entry-rules.d.ts +2 -0
- package/dist/entry-rules.js +6 -0
- package/dist/migrations/0070_platform_usage_tail.sql +12 -0
- package/dist/modules/assistants.js +32 -1
- package/dist/org.d.ts +9 -2
- package/dist/org.js +14 -1
- package/dist/plan/blue-green-apply.js +8 -0
- package/dist/plan/broker-entries.d.ts +14 -8
- package/dist/plan/broker-entries.js +14 -2
- package/dist/plan/diff.js +28 -9
- package/dist/plan/edge-access.d.ts +9 -0
- package/dist/plan/edge-access.js +20 -0
- package/dist/plan/entry-auth-events.d.ts +53 -0
- package/dist/plan/entry-auth-events.js +112 -0
- package/dist/plan/executor.d.ts +4 -0
- package/dist/plan/executor.js +97 -14
- package/dist/plan/machine-paths.d.ts +1 -1
- package/dist/plan/machine-paths.js +6 -6
- 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/stage-env.js +1 -0
- package/dist/plan/stage-isolation.js +1 -0
- package/dist/plan/usage-tail-worker.d.ts +29 -0
- package/dist/plan/usage-tail-worker.js +105 -0
- package/dist/plan/worker-app.d.ts +1 -1
- package/dist/plan/worker-app.js +21 -0
- package/dist/plan/workspace-sign-in.d.ts +2 -7
- package/dist/plan/workspace-sign-in.js +5 -1
- 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
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';
|
|
@@ -43,6 +43,7 @@ import { createEdgeZones } from './edge-zones.js';
|
|
|
43
43
|
import { AI_ENTRY_KEY_DAYS, AI_ENTRY_RENEW_WINDOW_DAYS, entryAiAppliedScopeKey, entryAiClampedScopeKey, entryAiExpiresScopeKey, entryAiPendingExpiresScopeKey, entryAiRetiredScopeKey, entryAiUndeliveredScopeKey, entryAiUnrevokedScopeKey, entryAiWebhookPendingScopeKey, entryAiWrittenScopeKey, parseIdList, } from './entry-ai-key.js';
|
|
44
44
|
import { ENTRY_ANALYTICS_KEY_DELIVERY_ATTEMPTS, entryAnalyticsAppliedScopeKey, entryAnalyticsRetiredScopeKey, entryAnalyticsUndeliveredScopeKey, stageAnalyticsKeyName, } from './entry-analytics-key.js';
|
|
45
45
|
import { applyEntryAudit } from './entry-audit.js';
|
|
46
|
+
import { AUTH_EVENTS_ENV, authEventsAppliedScopeKey, authEventsMarker, authEventsWanted, ensureAuthEventsSecret, heldAuthEventsSecret, } from './entry-auth-events.js';
|
|
46
47
|
import { CUSTOMER_GRANTS_CREDENTIAL_ENV, CUSTOMER_GRANTS_ENV, CUSTOMER_GRANTS_WRITTEN_SCOPE_KEY, customerGrantsEntries, customerGrantsJson, customerGrantsMintedScopeKey, customerGrantsRef, customerGrantsToDrop, ensureCustomerGrantsCredential, wantedCustomerGrants, } from './entry-customer-grants.js';
|
|
47
48
|
import { BOULE_EDGE_ENTRY_KEY, EDGE_ENTRY_KEY_DAYS, EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY, EDGE_SLUG_APPLIED_SCOPE_KEY, ENTRY_EDGE_KEY_DELIVERY_ATTEMPTS, bouleEdgeKeyWanted, edgeKeyAppRecordKey, edgeKeyDomain, entryEdgeMintedAtScopeKey, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
48
49
|
import { FILES_AGORA_KEY_SCOPE_KEY, FILES_AGORA_UNDELIVERED_SCOPE_KEY, FILES_KEY_DAYS, FILES_KEY_RENEW_WINDOW_DAYS, FILES_ORG_OWNER_SCOPE_KEY, FILES_ROOT_EXPIRES_SCOPE_KEY, FILES_ROOT_PREVIOUS_SCOPE_KEY, FILES_ROOT_UNDELIVERED_SCOPE_KEY, FILES_ROOT_UNREVOKED_SCOPE_KEY, entryFilesAppliedScopeKey, entryFilesExpiresScopeKey, entryFilesMinterScopeKey, entryFilesPendingExpiresScopeKey, entryFilesRetiredScopeKey, entryFilesUndeliveredScopeKey, entryFilesUnrevokedScopeKey, entryFilesWrittenScopeKey, filesOwnerUserId, parseIdList as parseFilesIdList, } from './entry-files-key.js';
|
|
@@ -3485,6 +3486,21 @@ export class FoundryStepExecutor {
|
|
|
3485
3486
|
});
|
|
3486
3487
|
env.push({ key: 'WORKSPACES_API_KEY', value: workspacesKey });
|
|
3487
3488
|
}
|
|
3489
|
+
if (authEventsWanted(entry)) {
|
|
3490
|
+
env.push({
|
|
3491
|
+
key: AUTH_EVENTS_ENV,
|
|
3492
|
+
value: await ensureAuthEventsSecret({
|
|
3493
|
+
orgId: org.id,
|
|
3494
|
+
slug: org.slug,
|
|
3495
|
+
entryKey: workspaceClientKey(entry.key),
|
|
3496
|
+
heldKeys,
|
|
3497
|
+
recorded: () => context.recordedIds(),
|
|
3498
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
3499
|
+
persist: () => context.persistScopeIds(),
|
|
3500
|
+
secret: (value) => void this.handledSecrets.add(value),
|
|
3501
|
+
}),
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3488
3504
|
const workspacesKeys = [];
|
|
3489
3505
|
for (const live of org.hosting.entries.filter((e) => e.uses?.auth?.workspaces)) {
|
|
3490
3506
|
const value = await heldKeys?.resolve(workspacesKeyRef(org.id, live.key));
|
|
@@ -3537,7 +3553,10 @@ export class FoundryStepExecutor {
|
|
|
3537
3553
|
}
|
|
3538
3554
|
this.handledSecrets.add(appKey);
|
|
3539
3555
|
const clientKey = workspaceClientKey(entry.key);
|
|
3540
|
-
await this.writeAppBrokerEntry(org, broker, clientKey, wanted.want, appKey, context,
|
|
3556
|
+
await this.writeAppBrokerEntry(org, broker, clientKey, wanted.want, appKey, context, {
|
|
3557
|
+
...(wanted.want.workspaces ? await this.workspacesInputs(org, clientKey) : {}),
|
|
3558
|
+
...(await this.eventsInput(org, clientKey, wanted.want)),
|
|
3559
|
+
});
|
|
3541
3560
|
return {};
|
|
3542
3561
|
}
|
|
3543
3562
|
/**
|
|
@@ -4336,6 +4355,7 @@ export class FoundryStepExecutor {
|
|
|
4336
4355
|
context.recordScopeId(appEnvAppliedScopeKey(entry.key), appEnv.applied);
|
|
4337
4356
|
if (entry.uses?.auth) {
|
|
4338
4357
|
context.recordScopeId(entryAuthEnvAppliedScopeKey(entry.key), appEnv.authApplied);
|
|
4358
|
+
context.recordScopeId(authEventsAppliedScopeKey(entry.key), appEnv.eventsApplied);
|
|
4339
4359
|
}
|
|
4340
4360
|
if (dbPush.pushed)
|
|
4341
4361
|
context.recordScopeId(dbEnvAppliedScopeKey(entry.key), '1');
|
|
@@ -4948,6 +4968,22 @@ export class FoundryStepExecutor {
|
|
|
4948
4968
|
});
|
|
4949
4969
|
envs.push({ key: 'WORKSPACES_API_KEY', value: workspacesKey });
|
|
4950
4970
|
}
|
|
4971
|
+
if (authEventsWanted(entry)) {
|
|
4972
|
+
const eventsSecret = await ensureAuthEventsSecret({
|
|
4973
|
+
orgId: org.id,
|
|
4974
|
+
slug: org.slug,
|
|
4975
|
+
entryKey: entry.key,
|
|
4976
|
+
heldKeys: this.ports.heldKeys,
|
|
4977
|
+
recorded: () => context.recordedIds(),
|
|
4978
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
4979
|
+
persist: () => context.persistScopeIds(),
|
|
4980
|
+
secret: (value) => void this.handledSecrets.add(value),
|
|
4981
|
+
});
|
|
4982
|
+
envs.push({ key: AUTH_EVENTS_ENV, value: eventsSecret });
|
|
4983
|
+
}
|
|
4984
|
+
else if (recorded[authEventsAppliedScopeKey(entry.key)] === 'on') {
|
|
4985
|
+
await this.dropAuthEventsEnv(appId);
|
|
4986
|
+
}
|
|
4951
4987
|
}
|
|
4952
4988
|
else {
|
|
4953
4989
|
const zitadelOrgId = recorded.zitadel_org;
|
|
@@ -5024,11 +5060,28 @@ export class FoundryStepExecutor {
|
|
|
5024
5060
|
changed: applied !== (recorded[appEnvAppliedScopeKey(entry.key)] ?? '{}') ||
|
|
5025
5061
|
heldChanged ||
|
|
5026
5062
|
(Boolean(entry.uses?.auth) &&
|
|
5027
|
-
authApplied !== (recorded[entryAuthEnvAppliedScopeKey(entry.key)] ?? '{}')
|
|
5063
|
+
(authApplied !== (recorded[entryAuthEnvAppliedScopeKey(entry.key)] ?? '{}') ||
|
|
5064
|
+
authEventsMarker(entry) !== (recorded[authEventsAppliedScopeKey(entry.key)] ?? ''))),
|
|
5028
5065
|
applied,
|
|
5029
5066
|
authApplied,
|
|
5067
|
+
eventsApplied: authEventsMarker(entry),
|
|
5030
5068
|
};
|
|
5031
5069
|
}
|
|
5070
|
+
/** Takes `AUTH_EVENTS_SECRET` out of the app's env once the entry stops declaring events. The held value stays: the held store has no delete. */
|
|
5071
|
+
async dropAuthEventsEnv(appId) {
|
|
5072
|
+
const rows = await this.ports.coolify.listEnvs(appId);
|
|
5073
|
+
for (const row of rows) {
|
|
5074
|
+
if (row.key !== AUTH_EVENTS_ENV || !row.uuid)
|
|
5075
|
+
continue;
|
|
5076
|
+
try {
|
|
5077
|
+
await this.ports.coolify.deleteEnv(appId, row.uuid);
|
|
5078
|
+
}
|
|
5079
|
+
catch (error) {
|
|
5080
|
+
if (!isNotFoundError(error))
|
|
5081
|
+
throw error;
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
5032
5085
|
/**
|
|
5033
5086
|
* The install's edge label token: read from the held keys, else minted and held first, so a crash
|
|
5034
5087
|
* after the `put` hands the same value out on the next apply. Read once more after the `put`, so two
|
|
@@ -5133,7 +5186,9 @@ export class FoundryStepExecutor {
|
|
|
5133
5186
|
if (entry.workerApp) {
|
|
5134
5187
|
// No Coolify app: the key is the held AUTH_APP_KEY the Worker app was given.
|
|
5135
5188
|
const heldKey = await heldBrokerKey(this.ports.heldKeys, org, entry.key);
|
|
5136
|
-
await this.writeAppBrokerEntry(org, broker, entry.key, wanted.want, heldKey, context
|
|
5189
|
+
await this.writeAppBrokerEntry(org, broker, entry.key, wanted.want, heldKey, context, {
|
|
5190
|
+
...(await this.eventsInput(org, entry.key, wanted.want)),
|
|
5191
|
+
});
|
|
5137
5192
|
return {};
|
|
5138
5193
|
}
|
|
5139
5194
|
const appId = recorded[`coolify:app:${action.key}`];
|
|
@@ -5144,9 +5199,20 @@ export class FoundryStepExecutor {
|
|
|
5144
5199
|
if (!appKey || appKey.length < 32) {
|
|
5145
5200
|
throw new Error(`coolify:broker-entry for ${org.slug}: app ${appId} has no AUTH_APP_KEY yet`);
|
|
5146
5201
|
}
|
|
5147
|
-
await this.writeAppBrokerEntry(org, broker, entry.key, wanted.want, appKey, context,
|
|
5202
|
+
await this.writeAppBrokerEntry(org, broker, entry.key, wanted.want, appKey, context, {
|
|
5203
|
+
...(wanted.want.workspaces ? await this.workspacesInputs(org, entry.key) : {}),
|
|
5204
|
+
...(await this.eventsInput(org, entry.key, wanted.want)),
|
|
5205
|
+
});
|
|
5148
5206
|
return {};
|
|
5149
5207
|
}
|
|
5208
|
+
/** The held events secret of a broker entry that declares events, registered for redaction; nothing when it declares none. */
|
|
5209
|
+
async eventsInput(org, entryKey, want) {
|
|
5210
|
+
if (!want.mcp?.events)
|
|
5211
|
+
return {};
|
|
5212
|
+
const eventsSecret = await heldAuthEventsSecret(this.ports.heldKeys, org, entryKey);
|
|
5213
|
+
this.handledSecrets.add(eventsSecret);
|
|
5214
|
+
return { eventsSecret };
|
|
5215
|
+
}
|
|
5150
5216
|
/**
|
|
5151
5217
|
* What the broker entry needs beyond its want when it declares workspaces: the hash of the held key (the key
|
|
5152
5218
|
* itself is registered for redaction and goes no further) and the operator's held database connection string.
|
|
@@ -5234,13 +5300,13 @@ export class FoundryStepExecutor {
|
|
|
5234
5300
|
return {};
|
|
5235
5301
|
}
|
|
5236
5302
|
/** The shared tail of both broker-entry steps: read the app's key, write the entry, record what was written. */
|
|
5237
|
-
async writeAppBrokerEntry(_org, broker, entryKey, want, appKey, context,
|
|
5303
|
+
async writeAppBrokerEntry(_org, broker, entryKey, want, appKey, context, extra = {}) {
|
|
5238
5304
|
const previousRaw = context.recordedIds()[brokerEntryAppliedScopeKey(entryKey)];
|
|
5239
5305
|
const result = await writeBrokerEntry(broker, {
|
|
5240
5306
|
want,
|
|
5241
5307
|
appKey,
|
|
5242
5308
|
previous: previousRaw ? JSON.parse(previousRaw) : null,
|
|
5243
|
-
...
|
|
5309
|
+
...extra,
|
|
5244
5310
|
});
|
|
5245
5311
|
context.recordScopeId(brokerEntryWarningsScopeKey(entryKey), JSON.stringify(result.invalidForeign));
|
|
5246
5312
|
// Last: a run that failed above plans the step again.
|
|
@@ -10787,6 +10853,7 @@ export class FoundryStepExecutor {
|
|
|
10787
10853
|
await edge.revokeKey(org.id, id);
|
|
10788
10854
|
}
|
|
10789
10855
|
catch (error) {
|
|
10856
|
+
edgeTooOld(error, at);
|
|
10790
10857
|
// A 404 means it is already gone -- recorded as retired below,
|
|
10791
10858
|
// the same as a successful revoke. Anything else is a real
|
|
10792
10859
|
// failure: thrown, not swallowed, so the key stays live and the
|
|
@@ -10856,18 +10923,26 @@ export class FoundryStepExecutor {
|
|
|
10856
10923
|
await edge.revokeKey(org.id, staleUndelivered);
|
|
10857
10924
|
}
|
|
10858
10925
|
catch (error) {
|
|
10926
|
+
edgeTooOld(error, at);
|
|
10859
10927
|
if (!isNotFoundError(error))
|
|
10860
10928
|
throw error;
|
|
10861
10929
|
}
|
|
10862
10930
|
context.recordScopeId(entryEdgeUndeliveredScopeKey(entryKey), '');
|
|
10863
10931
|
await context.persistScopeIds();
|
|
10864
10932
|
}
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
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
|
+
}
|
|
10871
10946
|
context.recordScopeId(entryEdgeUndeliveredScopeKey(entryKey), minted.id);
|
|
10872
10947
|
await context.persistScopeIds();
|
|
10873
10948
|
const envs = [
|
|
@@ -11028,6 +11103,7 @@ export class FoundryStepExecutor {
|
|
|
11028
11103
|
await edge.setOrganisationSlug(org.id, slug);
|
|
11029
11104
|
}
|
|
11030
11105
|
catch (e) {
|
|
11106
|
+
edgeTooOld(e, `edge:slug for ${org.slug}`);
|
|
11031
11107
|
if (e instanceof EdgeApiError && e.code === 'conflict') {
|
|
11032
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`);
|
|
11033
11109
|
}
|
|
@@ -11047,7 +11123,13 @@ export class FoundryStepExecutor {
|
|
|
11047
11123
|
throw new Error(`edge:reserved-labels for ${org.slug}: this executor was built without an EdgePort (#317)`);
|
|
11048
11124
|
}
|
|
11049
11125
|
const labels = action.desired.labels;
|
|
11050
|
-
|
|
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
|
+
}
|
|
11051
11133
|
context.recordScopeId(EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY, JSON.stringify(labels));
|
|
11052
11134
|
return {};
|
|
11053
11135
|
}
|
|
@@ -11592,6 +11674,7 @@ export class FoundryStepExecutor {
|
|
|
11592
11674
|
await edge.revokeKey(org.id, target.id);
|
|
11593
11675
|
}
|
|
11594
11676
|
catch (error) {
|
|
11677
|
+
edgeTooOld(error, `edge:key '${target.key}' for ${org.slug}`);
|
|
11595
11678
|
// Already gone -- the same already-retired key a retire step
|
|
11596
11679
|
// handled before org destroy ever ran can still reach here
|
|
11597
11680
|
// (#317 review); a 404 is success, not a reason to block
|
|
@@ -2,7 +2,7 @@ import { type DeclaredOrg, type HostingEntry } from '../org.js';
|
|
|
2
2
|
import type { DraftStep } from './diff.js';
|
|
3
3
|
/**
|
|
4
4
|
* F11: the paths of a gated entry that a machine calls, not a person: the ai service posts to
|
|
5
|
-
* `uses.ai.webhookPath`, Boule calls `/api/customer-grants`. A gate (`gates.ts`) stops a machine, so each of
|
|
5
|
+
* `uses.ai.webhookPath`, Boule calls `/api/customer-grants`, the broker posts signed assistant events to `uses.auth.mcp.events.path`. A gate (`gates.ts`) stops a machine, so each of
|
|
6
6
|
* these paths on each gated host gets its own path-scoped Access application (`<host><path>`) with a bypass
|
|
7
7
|
* policy, the opposite of a private page's path gate. Cloudflare applies the most specific application, so
|
|
8
8
|
* the bypass wins over the host's whole-host gate. The app itself checks the machine's credential.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isEdgePath } from '../entry-rules.js';
|
|
2
3
|
import { isLiveEntry } from '../org.js';
|
|
3
4
|
import { resolveGateHostnames } from './gates.js';
|
|
4
5
|
import { entryHostnames, workspaceHost } from './hostnames.js';
|
|
@@ -6,7 +7,7 @@ import { appStepKey } from './platform-diff.js';
|
|
|
6
7
|
import { workspaceEntries } from './workspace.js';
|
|
7
8
|
/**
|
|
8
9
|
* F11: the paths of a gated entry that a machine calls, not a person: the ai service posts to
|
|
9
|
-
* `uses.ai.webhookPath`, Boule calls `/api/customer-grants`. A gate (`gates.ts`) stops a machine, so each of
|
|
10
|
+
* `uses.ai.webhookPath`, Boule calls `/api/customer-grants`, the broker posts signed assistant events to `uses.auth.mcp.events.path`. A gate (`gates.ts`) stops a machine, so each of
|
|
10
11
|
* these paths on each gated host gets its own path-scoped Access application (`<host><path>`) with a bypass
|
|
11
12
|
* policy, the opposite of a private page's path gate. Cloudflare applies the most specific application, so
|
|
12
13
|
* the bypass wins over the host's whole-host gate. The app itself checks the machine's credential.
|
|
@@ -21,17 +22,16 @@ export function entryMachinePaths(entry) {
|
|
|
21
22
|
paths.push(webhook);
|
|
22
23
|
if (entry.uses?.customerGrants)
|
|
23
24
|
paths.push(CUSTOMER_GRANTS_PATH);
|
|
25
|
+
const events = entry.uses?.auth?.mcp?.events?.path;
|
|
26
|
+
if (events !== undefined)
|
|
27
|
+
paths.push(events);
|
|
24
28
|
return [...new Set(paths)];
|
|
25
29
|
}
|
|
26
30
|
/** The step key stem of one machine bypass: a hash of host and path, so it fits a key and needs no escaping. */
|
|
27
31
|
export function machineBypassKey(host, path) {
|
|
28
32
|
return `mp-${createHash('sha256').update(`${host}\0${path}`).digest('hex').slice(0, 16)}`;
|
|
29
33
|
}
|
|
30
|
-
|
|
31
|
-
const EDGE_PATH = /^(\/[a-z0-9._~-]+)+$/;
|
|
32
|
-
const edgeAccepts = (path) => path.length <= 200 &&
|
|
33
|
-
EDGE_PATH.test(path) &&
|
|
34
|
-
!path.split('/').some((s) => s === '.' || s === '..');
|
|
34
|
+
const edgeAccepts = isEdgePath;
|
|
35
35
|
/** The recorded `host/path` a machine bypass was made for, by its step key stem, read for the delete's wording. */
|
|
36
36
|
export const machineBypassScopeKey = (stem) => `machine_bypass_${stem}`;
|
|
37
37
|
/**
|
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
|