@wtfalch/foundry 0.65.0 → 0.67.0
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/apps/cloudflare.d.ts +5 -0
- package/dist/apps/deploy.js +33 -1
- package/dist/apps/migrations.d.ts +3 -0
- package/dist/apps/migrations.js +32 -7
- package/dist/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/entry-rules.d.ts +11 -0
- package/dist/entry-rules.js +70 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/migrations/0069_release_events.sql +16 -0
- package/dist/modules/analytics.d.ts +8 -0
- package/dist/modules/analytics.js +28 -0
- package/dist/modules/database.js +22 -3
- package/dist/modules/index.js +2 -0
- package/dist/native-build.d.ts +16 -12
- package/dist/native-build.js +16 -12
- package/dist/org.d.ts +9 -1
- package/dist/org.js +4 -1
- package/dist/plan/analytics-boule.d.ts +17 -0
- package/dist/plan/analytics-boule.js +43 -0
- package/dist/plan/analytics.d.ts +58 -0
- package/dist/plan/analytics.js +89 -0
- package/dist/plan/boule.js +2 -0
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +4 -0
- package/dist/plan/diff.js +103 -3
- package/dist/plan/entry-analytics-key.d.ts +35 -0
- package/dist/plan/entry-analytics-key.js +134 -0
- package/dist/plan/executor.d.ts +79 -6
- package/dist/plan/executor.js +379 -19
- package/dist/plan/hostnames.js +4 -0
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.js +3 -0
- package/dist/plan/login-host.d.ts +8 -0
- package/dist/plan/login-host.js +8 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +12 -0
- package/dist/plan/plan-apply.js +40 -1
- package/dist/plan/platform-diff.d.ts +17 -1
- package/dist/plan/platform-diff.js +40 -2
- package/dist/plan/platform-org.d.ts +6 -0
- package/dist/plan/platform-providers.d.ts +8 -6
- package/dist/plan/platform-providers.js +56 -10
- package/dist/plan/providers-readers.js +16 -1
- package/dist/plan/release-host.d.ts +30 -1
- package/dist/plan/release-host.js +84 -41
- package/dist/plan/release-series-reader.d.ts +26 -0
- package/dist/plan/release-series-reader.js +40 -0
- package/dist/plan/sign-in-archon.d.ts +1 -1
- package/dist/plan/sign-in-archon.js +2 -2
- package/dist/plan/stage-services.d.ts +3 -1
- package/dist/plan/stage-services.js +2 -1
- package/dist/plan/uptime-worker.js +53 -3
- package/dist/plan/worker-app-database.d.ts +12 -1
- package/dist/plan/worker-app-database.js +13 -0
- package/dist/plan/worker-app.d.ts +27 -2
- package/dist/plan/worker-app.js +52 -7
- package/dist/plan/workspace.d.ts +2 -2
- package/dist/plan/workspace.js +3 -3
- package/dist/release/decide.d.ts +2 -0
- package/dist/release/decide.js +67 -10
- package/dist/release/events.d.ts +13 -0
- package/dist/release/events.js +67 -0
- package/dist/release/health.d.ts +5 -0
- package/dist/release/health.js +3 -3
- package/dist/release/run.d.ts +7 -2
- package/dist/release/run.js +8 -0
- package/dist/release/series.d.ts +58 -0
- package/dist/release/series.js +65 -0
- package/dist/release/state.d.ts +58 -1
- package/dist/release/state.js +45 -0
- package/dist/release/types.d.ts +44 -0
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +5 -2
- package/dist/store/postgres.js +29 -3
- package/dist/store/schema.d.ts +112 -0
- package/dist/store/schema.js +9 -0
- package/dist/worker-app-config.d.ts +22 -1
- package/dist/worker-app-config.js +73 -15
- package/dist/worker-app-migrations.d.ts +6 -2
- package/dist/worker-app-migrations.js +1 -0
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ReleaseDeps, type StageCopyJob, type StageCopyOutcome } from '../release/run.js';
|
|
2
|
+
import { type ReleaseSeriesQuery } from '../release/series.js';
|
|
2
3
|
import { type ReleaseStore } from '../release/state.js';
|
|
4
|
+
import type { ReleaseSeries } from '../release/types.js';
|
|
3
5
|
import { type BlueGreenSubject, type PinHeldKeys } from './blue-green.js';
|
|
4
6
|
import { type DeployHealthRuntime } from './deploy-health.js';
|
|
5
7
|
import type { CoolifyPort } from './executor.js';
|
|
@@ -18,6 +20,11 @@ export interface ReleaseHost {
|
|
|
18
20
|
refreshStage(): Promise<void>;
|
|
19
21
|
/** Writes the recorded weights to the balancer on every server that has one. */
|
|
20
22
|
writeWeights(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Reporting's release series for `entry` (C2), read through the balancer like the health read. Null when
|
|
25
|
+
* `entry` is not this host's, the entry has no reporting, or the read failed.
|
|
26
|
+
*/
|
|
27
|
+
readSeries?(entry: string, query: ReleaseSeriesQuery): Promise<ReleaseSeries | null>;
|
|
21
28
|
}
|
|
22
29
|
export interface ReleaseHostInput {
|
|
23
30
|
subject: BlueGreenSubject;
|
|
@@ -47,6 +54,27 @@ export interface ReleaseHostInput {
|
|
|
47
54
|
/** A stage deploy waits at most this many polls of this long (10 minutes) for Coolify's deployment to finish. */
|
|
48
55
|
export declare const STAGE_DEPLOY_ATTEMPTS = 60;
|
|
49
56
|
export declare const STAGE_DEPLOY_INTERVAL_MS = 10000;
|
|
57
|
+
/** What the balancer read of a subject needs: its address, its pin key and its health token. */
|
|
58
|
+
interface SubjectRead {
|
|
59
|
+
subject: BlueGreenSubject;
|
|
60
|
+
healthToken: ReleaseHostInput['healthToken'];
|
|
61
|
+
secret: ReleaseHostInput['secret'];
|
|
62
|
+
pinRef: string;
|
|
63
|
+
heldKeys?: PinHeldKeys;
|
|
64
|
+
fetch?: typeof fetch;
|
|
65
|
+
renew?: () => Promise<void>;
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
68
|
+
type Recorded = () => Readonly<Record<string, string>>;
|
|
69
|
+
/**
|
|
70
|
+
* Reporting's release series of one subject (C2), read through the balancer like the health read. Null when the
|
|
71
|
+
* subject has no reporting, the token is not held, or the read failed. Used by a release run and, outside a run,
|
|
72
|
+
* by {@link readEntryReleaseSeries}.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readSubjectSeries(read: SubjectRead & {
|
|
75
|
+
recorded: Recorded;
|
|
76
|
+
query: ReleaseSeriesQuery;
|
|
77
|
+
}): Promise<ReleaseSeries | null>;
|
|
50
78
|
export declare function createReleaseHost(input: ReleaseHostInput): ReleaseHost;
|
|
51
79
|
/**
|
|
52
80
|
* One release run for one subject, writing into `context` (the live recorded ids of the run that holds
|
|
@@ -56,7 +84,7 @@ export declare function createReleaseHost(input: ReleaseHostInput): ReleaseHost;
|
|
|
56
84
|
* (like the next apply, through its `bluegreen-lb` step) writes it again.
|
|
57
85
|
*/
|
|
58
86
|
export declare function runSubjectRelease(input: {
|
|
59
|
-
store: Pick<ReleaseStore, 'openRequests' | 'consumeRequests'
|
|
87
|
+
store: Pick<ReleaseStore, 'openRequests' | 'consumeRequests'> & Partial<Pick<ReleaseStore, 'appendEvents'>>;
|
|
60
88
|
scopeOwnerId: string;
|
|
61
89
|
subject: BlueGreenSubject;
|
|
62
90
|
host: ReleaseHost;
|
|
@@ -90,3 +118,4 @@ export declare function finishStageCopy(input: {
|
|
|
90
118
|
outcome: StageCopyOutcome;
|
|
91
119
|
refused: string[];
|
|
92
120
|
}>;
|
|
121
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { checkColorAddress, readReleaseHealth } from '../release/health.js';
|
|
3
3
|
import { runRelease, runStageCopy, settleStageCopy, } from '../release/run.js';
|
|
4
|
+
import { fetchReleaseSeries } from '../release/series.js';
|
|
4
5
|
import { releaseKeys } from '../release/state.js';
|
|
5
6
|
import { applyBlueGreenWeights, copiedEnv } from './blue-green-apply.js';
|
|
6
7
|
import { ACTIONS_KEY_ENV, DEFAULT_WEIGHTS, LB_COLOR_HEADER, LB_PIN_HEADER, bgKey, formatWeights, mintPinKey, parseWeights, readPinKey, reportingEnv, } from './blue-green.js';
|
|
@@ -20,6 +21,68 @@ function statusOf(status) {
|
|
|
20
21
|
return 'other';
|
|
21
22
|
}
|
|
22
23
|
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
const pinInputOf = (read, recorded) => ({
|
|
25
|
+
id: read.subject.id,
|
|
26
|
+
ref: read.pinRef,
|
|
27
|
+
recorded,
|
|
28
|
+
secret: read.secret,
|
|
29
|
+
heldKeys: read.heldKeys ?? { resolve: async () => null },
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Where the runner reaches a color: the entry's first public hostname, pinned to the color by the balancer's
|
|
33
|
+
* header router. The balancer routes the pinned request to that color whatever its weight.
|
|
34
|
+
*/
|
|
35
|
+
async function colorAddress(read, recorded, color) {
|
|
36
|
+
const hostname = read.subject.hostnames.find((name) => !name.startsWith('*'));
|
|
37
|
+
if (!hostname)
|
|
38
|
+
throw new Error(`${read.label}: the app has no public hostname to check through`);
|
|
39
|
+
const pinKey = await readPinKey(pinInputOf(read, recorded));
|
|
40
|
+
if (!pinKey)
|
|
41
|
+
throw new Error(`${read.label}: the balancer's pin key is not held`);
|
|
42
|
+
return {
|
|
43
|
+
address: `https://${hostname}`,
|
|
44
|
+
headers: { [LB_COLOR_HEADER]: color, [LB_PIN_HEADER]: pinKey },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** The color a health read goes to: the one being released, else the live one. */
|
|
48
|
+
function healthColorOf(ids, id) {
|
|
49
|
+
try {
|
|
50
|
+
const window = JSON.parse(ids[bgKey.window(id)] ?? '{}');
|
|
51
|
+
if (window.release?.to === 'blue' || window.release?.to === 'green')
|
|
52
|
+
return window.release.to;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// A damaged window reads as no release; the runner rewrites it.
|
|
56
|
+
}
|
|
57
|
+
return ids[bgKey.live(id)] === 'green' ? 'green' : 'blue';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Reporting's release series of one subject (C2), read through the balancer like the health read. Null when the
|
|
61
|
+
* subject has no reporting, the token is not held, or the read failed. Used by a release run and, outside a run,
|
|
62
|
+
* by {@link readEntryReleaseSeries}.
|
|
63
|
+
*/
|
|
64
|
+
export async function readSubjectSeries(read) {
|
|
65
|
+
if (!read.subject.reporting)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
await read.renew?.();
|
|
69
|
+
const token = await read.healthToken();
|
|
70
|
+
if (!token)
|
|
71
|
+
return null;
|
|
72
|
+
read.secret(token);
|
|
73
|
+
return await fetchReleaseSeries({
|
|
74
|
+
...(await colorAddress(read, read.recorded, healthColorOf(read.recorded(), read.subject.id))),
|
|
75
|
+
token,
|
|
76
|
+
query: read.query,
|
|
77
|
+
...(read.fetch ? { fetchImpl: read.fetch } : {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// No hostname or pin key yet: the page shows no chart. The cause can name a hostname, so it is not logged.
|
|
82
|
+
console.error(`${read.label}: reading the release series failed`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
23
86
|
export function createReleaseHost(input) {
|
|
24
87
|
const { subject, context, coolify } = input;
|
|
25
88
|
const id = subject.id;
|
|
@@ -43,41 +106,9 @@ export function createReleaseHost(input) {
|
|
|
43
106
|
};
|
|
44
107
|
const colorApps = (color) => colorAppsOnServers(color).map((a) => a.appId);
|
|
45
108
|
const primaryColorApp = (color) => color === 'blue' ? input.primaryBlueAppId : recorded()[bgKey.app(id, input.primaryUuid, color)];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*/
|
|
50
|
-
const pinInput = () => ({
|
|
51
|
-
id,
|
|
52
|
-
ref: input.pinRef,
|
|
53
|
-
recorded,
|
|
54
|
-
secret: input.secret,
|
|
55
|
-
heldKeys: input.heldKeys ?? { resolve: async () => null },
|
|
56
|
-
});
|
|
57
|
-
const addressOf = async (color) => {
|
|
58
|
-
const hostname = subject.hostnames.find((name) => !name.startsWith('*'));
|
|
59
|
-
if (!hostname)
|
|
60
|
-
throw new Error(`${input.label}: the app has no public hostname to check through`);
|
|
61
|
-
const pinKey = await readPinKey(pinInput());
|
|
62
|
-
if (!pinKey)
|
|
63
|
-
throw new Error(`${input.label}: the balancer's pin key is not held`);
|
|
64
|
-
return {
|
|
65
|
-
address: `https://${hostname}`,
|
|
66
|
-
headers: { [LB_COLOR_HEADER]: color, [LB_PIN_HEADER]: pinKey },
|
|
67
|
-
};
|
|
68
|
-
};
|
|
69
|
-
/** The color a health read goes to: the one being released, else the live one. */
|
|
70
|
-
const healthColor = () => {
|
|
71
|
-
try {
|
|
72
|
-
const window = JSON.parse(recorded()[bgKey.window(id)] ?? '{}');
|
|
73
|
-
if (window.release?.to === 'blue' || window.release?.to === 'green')
|
|
74
|
-
return window.release.to;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// A damaged window reads as no release; the runner rewrites it.
|
|
78
|
-
}
|
|
79
|
-
return recorded()[bgKey.live(id)] === 'green' ? 'green' : 'blue';
|
|
80
|
-
};
|
|
109
|
+
const pinInput = () => pinInputOf(input, recorded);
|
|
110
|
+
const addressOf = (color) => colorAddress(input, recorded, color);
|
|
111
|
+
const healthColor = () => healthColorOf(recorded(), id);
|
|
81
112
|
const deployTo = async (color, digest) => {
|
|
82
113
|
if (!COMMIT.test(digest))
|
|
83
114
|
throw new Error(`${input.label}: the release is not a built commit`);
|
|
@@ -250,6 +281,11 @@ export function createReleaseHost(input) {
|
|
|
250
281
|
};
|
|
251
282
|
return {
|
|
252
283
|
deps,
|
|
284
|
+
async readSeries(entry, query) {
|
|
285
|
+
if (entry !== id)
|
|
286
|
+
return null;
|
|
287
|
+
return readSubjectSeries({ ...input, recorded, query });
|
|
288
|
+
},
|
|
253
289
|
async refreshStage() {
|
|
254
290
|
const stageId = recorded()[bgKey.app(id, input.primaryUuid, 'stage')];
|
|
255
291
|
if (!stageId || !coolify.listApplicationDeployments)
|
|
@@ -324,13 +360,7 @@ export async function runSubjectRelease(input) {
|
|
|
324
360
|
const result = await runRelease({
|
|
325
361
|
store: {
|
|
326
362
|
getScopeIds: async () => ({ ...ids() }),
|
|
327
|
-
addRequest: async () => {
|
|
328
|
-
throw new Error('the release runner takes requests; it does not add them');
|
|
329
|
-
},
|
|
330
363
|
openRequests: (query) => input.store.openRequests(query),
|
|
331
|
-
consumeRequests: async () => {
|
|
332
|
-
throw new Error('the release runner consumes requests after it persists');
|
|
333
|
-
},
|
|
334
364
|
},
|
|
335
365
|
orgId: input.scopeOwnerId,
|
|
336
366
|
entryKey: id,
|
|
@@ -347,6 +377,19 @@ export async function runSubjectRelease(input) {
|
|
|
347
377
|
context.recordScopeId(key, value);
|
|
348
378
|
}
|
|
349
379
|
await context.persist();
|
|
380
|
+
if (input.store.appendEvents && result.events.length > 0) {
|
|
381
|
+
try {
|
|
382
|
+
await input.store.appendEvents({
|
|
383
|
+
orgId: input.scopeOwnerId,
|
|
384
|
+
entryKey: id,
|
|
385
|
+
events: result.events,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// The history is a record, not a gate: losing a line must not leave the run's requests open to replay.
|
|
390
|
+
console.error(`release ${id}: recording ${result.events.length} history lines failed`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
350
393
|
await input.store.consumeRequests(result.requestIds);
|
|
351
394
|
const after = parseWeights(ids()[bgKey.weights(id)]);
|
|
352
395
|
// Also when a balancer lags the weights (an earlier write failed), so the next tick retries it.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReleaseSeriesQuery } from '../release/series.js';
|
|
2
|
+
import type { ReleaseStore } from '../release/state.js';
|
|
3
|
+
import type { ReleaseSeries } from '../release/types.js';
|
|
4
|
+
import type { DeclaredOrgStore } from '../store.js';
|
|
5
|
+
import { type PinHeldKeys } from './blue-green.js';
|
|
6
|
+
/** What a request needs to read a series outside a release run: what Archon already holds. */
|
|
7
|
+
export interface EntrySeriesDeps {
|
|
8
|
+
orgs: Pick<DeclaredOrgStore, 'get'>;
|
|
9
|
+
/** The recorded ids of the org, as the release tick persists them (`createPostgresReleaseStore`). */
|
|
10
|
+
store: Pick<ReleaseStore, 'getScopeIds'>;
|
|
11
|
+
/** The held store: reads the entry's health token and the balancer's pin key. */
|
|
12
|
+
heldKeys: Pick<PinHeldKeys, 'resolve'>;
|
|
13
|
+
/** Registers a value for redaction before it is used. */
|
|
14
|
+
secret?: (value: string) => void;
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* C2 for a request: the series of one blue/green entry, read the way a release run reads it (the entry's
|
|
19
|
+
* first public hostname, the colour pin headers, the health token, a 15 s timeout, a 1 MiB cap). Null for an
|
|
20
|
+
* org or entry that is not declared blue/green, an app without reporting, a token or pin key not held, and any
|
|
21
|
+
* failed read; a failure logs fixed text only. Never mints and never writes.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readEntryReleaseSeries(deps: EntrySeriesDeps, input: {
|
|
24
|
+
orgId: string;
|
|
25
|
+
entryKey: string;
|
|
26
|
+
} & ReleaseSeriesQuery): Promise<ReleaseSeries | null>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { sharedSecretRef } from '../org.js';
|
|
2
|
+
import { blueGreenSubjects, healthTokenName, pinKeyName } from './blue-green.js';
|
|
3
|
+
import { readSubjectSeries } from './release-host.js';
|
|
4
|
+
/**
|
|
5
|
+
* C2 for a request: the series of one blue/green entry, read the way a release run reads it (the entry's
|
|
6
|
+
* first public hostname, the colour pin headers, the health token, a 15 s timeout, a 1 MiB cap). Null for an
|
|
7
|
+
* org or entry that is not declared blue/green, an app without reporting, a token or pin key not held, and any
|
|
8
|
+
* failed read; a failure logs fixed text only. Never mints and never writes.
|
|
9
|
+
*/
|
|
10
|
+
export async function readEntryReleaseSeries(deps, input) {
|
|
11
|
+
const label = `coolify:release:${input.entryKey}`;
|
|
12
|
+
try {
|
|
13
|
+
const org = await deps.orgs.get(input.orgId);
|
|
14
|
+
if (!org)
|
|
15
|
+
return null;
|
|
16
|
+
const ids = await deps.store.getScopeIds(input.orgId);
|
|
17
|
+
const subject = blueGreenSubjects(org, ids).find((s) => s.id === input.entryKey);
|
|
18
|
+
if (!subject)
|
|
19
|
+
return null;
|
|
20
|
+
return await readSubjectSeries({
|
|
21
|
+
subject,
|
|
22
|
+
recorded: () => ids,
|
|
23
|
+
healthToken: async () => (await deps.heldKeys.resolve(sharedSecretRef(org.id, healthTokenName(subject.id)))) ?? null,
|
|
24
|
+
secret: deps.secret ?? (() => undefined),
|
|
25
|
+
pinRef: sharedSecretRef(org.id, pinKeyName(subject.id)),
|
|
26
|
+
heldKeys: deps.heldKeys,
|
|
27
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
28
|
+
label,
|
|
29
|
+
query: {
|
|
30
|
+
from: input.from,
|
|
31
|
+
to: input.to,
|
|
32
|
+
...(input.bucket !== undefined ? { bucket: input.bucket } : {}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
console.error(`${label}: reading the release series failed`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -9,7 +9,7 @@ export declare const PLATFORM_ARCHON_IN_APP_SCOPE_KEY = "platform_archon_in_app"
|
|
|
9
9
|
/** The host a recorded {@link PLATFORM_ARCHON_IN_APP_SCOPE_KEY} names; `''` when none. */
|
|
10
10
|
export declare function archonInAppHost(recorded: Readonly<Record<string, string>>): string;
|
|
11
11
|
/** The ZITADEL calls Archon's in-app sign-in makes, on the owner company's own instance. */
|
|
12
|
-
export type ArchonInAppZitadel = Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri'> & Partial<Pick<ZitadelPort, 'allowPerAppLoginBaseUri'>>;
|
|
12
|
+
export type ArchonInAppZitadel = Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'listApps'> & Partial<Pick<ZitadelPort, 'allowPerAppLoginBaseUri' | 'listOrgs' | 'listProjects'>>;
|
|
13
13
|
interface ArchonInAppInput {
|
|
14
14
|
zitadel: ArchonInAppZitadel;
|
|
15
15
|
coolify: Pick<CoolifyPort, 'listEnvs' | 'bulkUpsertEnvs' | 'deploy'>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isNotFoundError, setOidcAppLoginBaseUri } from './executor.js';
|
|
1
|
+
import { allowPerAppLoginBaseUris, isNotFoundError, setOidcAppLoginBaseUri } from './executor.js';
|
|
2
2
|
import { editSignInBindings } from './sign-in-bindings.js';
|
|
3
3
|
import { SIGN_IN_BASE_PATH } from './sign-in.js';
|
|
4
4
|
/**
|
|
@@ -41,7 +41,7 @@ export async function archonInAppPoint(input) {
|
|
|
41
41
|
if (!zitadel.allowPerAppLoginBaseUri) {
|
|
42
42
|
throw new Error("platform:archon: the owner company's ZITADEL port cannot allow a per-app login base URI, so Archon cannot sign in on its own address");
|
|
43
43
|
}
|
|
44
|
-
await zitadel
|
|
44
|
+
await allowPerAppLoginBaseUris(zitadel, `https://${input.loginHost}/`);
|
|
45
45
|
await setOidcAppLoginBaseUri(zitadel, input.organisationId, input.projectId, input.appId, `https://${host}${SIGN_IN_BASE_PATH}/`);
|
|
46
46
|
}
|
|
47
47
|
/** First half of giving the address up: the client goes back to the company's login host. */
|
|
@@ -5,7 +5,7 @@ import { type StageKind } from './stage-isolation.js';
|
|
|
5
5
|
* minted for it only. A stage key is worthless on production, and production's key never reaches stage. Nothing
|
|
6
6
|
* here returns, logs or throws a secret: errors carry fixed text, ids and refs (CLAUDE.md, Platform steps).
|
|
7
7
|
*/
|
|
8
|
-
export type StageService = 'files' | 'ai' | 'jobs';
|
|
8
|
+
export type StageService = 'files' | 'ai' | 'jobs' | 'analytics';
|
|
9
9
|
export declare const STAGE_SERVICES: readonly StageService[];
|
|
10
10
|
/** One credential a service shows once: the env name stage holds it under, and its value. */
|
|
11
11
|
export interface StageSecret {
|
|
@@ -44,6 +44,8 @@ export interface StageServicesPort {
|
|
|
44
44
|
files?: StageServicePort;
|
|
45
45
|
ai?: StageServicePort;
|
|
46
46
|
jobs?: StageServicePort;
|
|
47
|
+
/** The company's analytics backend: stage's own site key (environment `stage`), shown once with the mint. */
|
|
48
|
+
analytics?: StageServicePort;
|
|
47
49
|
/** The id of production's jobs project, so stage's jobs key can be tried against it. */
|
|
48
50
|
prodJobsProjectId?: string;
|
|
49
51
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { bgKey } from './blue-green.js';
|
|
3
3
|
import { isAllowed } from './stage-isolation.js';
|
|
4
|
-
export const STAGE_SERVICES = ['files', 'ai', 'jobs'];
|
|
4
|
+
export const STAGE_SERVICES = ['files', 'ai', 'jobs', 'analytics'];
|
|
5
5
|
/** Fixed text and ids only: safe to log, record and return. */
|
|
6
6
|
export class StageServiceError extends Error {
|
|
7
7
|
name = 'StageServiceError';
|
|
@@ -14,6 +14,7 @@ const SERVICE_ENV = {
|
|
|
14
14
|
evidence: ['JOBS_SCHEDULER_URL', 'JOBS_SYNC_KEY', 'JOBS_SIGNING_SECRET'],
|
|
15
15
|
url: 'JOBS_SCHEDULER_URL',
|
|
16
16
|
},
|
|
17
|
+
analytics: { evidence: ['ANALYTICS_URL', 'ANALYTICS_SITE_KEY'], url: 'ANALYTICS_URL' },
|
|
17
18
|
};
|
|
18
19
|
/** The fixed reasons a used service is recorded absent. */
|
|
19
20
|
export const STAGE_SERVICE_ABSENT = {
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
2
|
import { EdgeApiError } from '@wtfalch/edge';
|
|
3
3
|
import { uptimeArtifactUrl } from '../platform.js';
|
|
4
|
-
import { PLATFORM_EDGE_INTERNAL_URL_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_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_CHAT_TOKEN_REF, PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_URL_REF, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_KV_TITLE, PLATFORM_UPTIME_OPTIONAL_SECRETS, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeSecretNames, uptimeVarsDigest, } from './platform-diff.js';
|
|
5
|
-
/**
|
|
6
|
-
|
|
4
|
+
import { PLATFORM_EDGE_INTERNAL_URL_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_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF, PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_CHAT_TOKEN_REF, PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_URL_REF, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_KV_TITLE, PLATFORM_UPTIME_OPTIONAL_SECRETS, PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF, PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeSecretNames, uptimeVarsDigest, } from './platform-diff.js';
|
|
5
|
+
/**
|
|
6
|
+
* The Worker's cron triggers, in the order of Archon's `workers/uptime/manifest.json`: the probe, the
|
|
7
|
+
* usage tick, the charge tick, the release tick and the audit seal tick. The usage and charge ticks
|
|
8
|
+
* stay off because the step gives the Worker no `USAGE_TOKEN` or `CHARGE_TOKEN`.
|
|
9
|
+
*/
|
|
10
|
+
const UPTIME_CRONS = [
|
|
11
|
+
'* * * * *',
|
|
12
|
+
'0 * * * *',
|
|
13
|
+
'15 3 * * *',
|
|
14
|
+
'*/10 * * * *',
|
|
15
|
+
'*/15 * * * *',
|
|
16
|
+
];
|
|
7
17
|
/** The date Archon's own Worker config used. */
|
|
8
18
|
const UPTIME_COMPATIBILITY_DATE = '2026-09-27';
|
|
9
19
|
/** How many times a delivery or a secrets write is tried before the step fails. */
|
|
@@ -124,6 +134,27 @@ async function attempt(what, task) {
|
|
|
124
134
|
}
|
|
125
135
|
throw new Error(`platform:uptime: ${what} failed after ${UPTIME_ATTEMPTS} attempts`);
|
|
126
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Holds a minted tick token under `ref`, the Worker token's pattern: the undelivered id is persisted
|
|
139
|
+
* before the value is held, the guard once it is. A guard with nothing held, or a marker without a
|
|
140
|
+
* guard, means the value is gone: a new one is minted and the old id dropped. Returns the held value
|
|
141
|
+
* and its id (the Worker secret's version label); the value goes nowhere else.
|
|
142
|
+
*/
|
|
143
|
+
async function holdTickToken(context, deps, recorded, keys) {
|
|
144
|
+
const guard = recorded[keys.idKey];
|
|
145
|
+
const heldValue = guard ? await deps.heldKeys.resolve(keys.ref) : null;
|
|
146
|
+
if (guard && heldValue)
|
|
147
|
+
return { id: guard, value: heldValue };
|
|
148
|
+
const id = randomUUID();
|
|
149
|
+
const value = randomBytes(32).toString('hex');
|
|
150
|
+
context.recordScopeId(keys.undeliveredKey, id);
|
|
151
|
+
await context.persist?.();
|
|
152
|
+
await attempt(`holding token ${id}`, () => deps.deliverSecret(keys.ref, value));
|
|
153
|
+
context.recordScopeId(keys.idKey, id);
|
|
154
|
+
context.recordScopeId(keys.undeliveredKey, '');
|
|
155
|
+
await context.persist?.();
|
|
156
|
+
return { id, value };
|
|
157
|
+
}
|
|
127
158
|
/** Removes an Access token edge made; one already gone is success. */
|
|
128
159
|
async function removeAccessToken(access, id, what) {
|
|
129
160
|
try {
|
|
@@ -165,6 +196,8 @@ export async function provisionPlatformUptime(context, deps) {
|
|
|
165
196
|
}
|
|
166
197
|
for (const ref of [
|
|
167
198
|
PLATFORM_UPTIME_TOKEN_REF,
|
|
199
|
+
PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF,
|
|
200
|
+
PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF,
|
|
168
201
|
PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF,
|
|
169
202
|
PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF,
|
|
170
203
|
]) {
|
|
@@ -175,6 +208,10 @@ export async function provisionPlatformUptime(context, deps) {
|
|
|
175
208
|
PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY,
|
|
176
209
|
PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY,
|
|
177
210
|
PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY,
|
|
211
|
+
PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY,
|
|
212
|
+
PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY,
|
|
213
|
+
PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY,
|
|
214
|
+
PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY,
|
|
178
215
|
]) {
|
|
179
216
|
context.recordScopeId(key, '');
|
|
180
217
|
}
|
|
@@ -280,6 +317,17 @@ export async function provisionPlatformUptime(context, deps) {
|
|
|
280
317
|
tokenId = newId;
|
|
281
318
|
token = minted;
|
|
282
319
|
}
|
|
320
|
+
// The two tick tokens, minted and held the same way, each its own value.
|
|
321
|
+
const releaseTick = await holdTickToken(context, deps, recorded, {
|
|
322
|
+
ref: PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF,
|
|
323
|
+
idKey: PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY,
|
|
324
|
+
undeliveredKey: PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY,
|
|
325
|
+
});
|
|
326
|
+
const auditSealTick = await holdTickToken(context, deps, recorded, {
|
|
327
|
+
ref: PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF,
|
|
328
|
+
idKey: PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY,
|
|
329
|
+
undeliveredKey: PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY,
|
|
330
|
+
});
|
|
283
331
|
// The Access service token pair, minted through edge. The id is recorded
|
|
284
332
|
// and persisted before the pair is held, the guard once both are held. A
|
|
285
333
|
// pair that may be lost (marker without guard, or guard with nothing held)
|
|
@@ -341,6 +389,8 @@ export async function provisionPlatformUptime(context, deps) {
|
|
|
341
389
|
const version = `pin-${uptime.sha256.slice(0, 12)}`;
|
|
342
390
|
const secrets = [
|
|
343
391
|
{ name: 'UPTIME_TOKEN', value: token, version: tokenId },
|
|
392
|
+
{ name: 'RELEASE_TICK_TOKEN', value: releaseTick.value, version: releaseTick.id },
|
|
393
|
+
{ name: 'AUDIT_SEAL_TICK_TOKEN', value: auditSealTick.value, version: auditSealTick.id },
|
|
344
394
|
{ name: 'CF_ACCESS_CLIENT_ID', value: clientId, version: accessId },
|
|
345
395
|
{ name: 'CF_ACCESS_CLIENT_SECRET', value: clientSecret, version: accessId },
|
|
346
396
|
// A ping URL works as a capability: a secret, not a plain text binding.
|
|
@@ -18,7 +18,7 @@ export { type DatabaseMigration, MIGRATIONS_ASSET_NAME, WORKER_APP_MIGRATION_TIM
|
|
|
18
18
|
* database it cannot use:
|
|
19
19
|
* Access app and service token -> tunnel rule and DNS -> `<db>_rt` password -> release
|
|
20
20
|
* `migrations.json` (its sha256 checked against `database.migrationsSha256`) applied with the owner credentials -> runtime role (UPDATE, DELETE and
|
|
21
|
-
* TRUNCATE revoked on `appendOnly`) -> Hyperdrive config with the runtime credentials.
|
|
21
|
+
* TRUNCATE revoked on `appendOnly`, DELETE and TRUNCATE on `noDelete`) -> Hyperdrive config with the runtime credentials.
|
|
22
22
|
* A failed migration throws, so the Worker is not uploaded.
|
|
23
23
|
*/
|
|
24
24
|
/** Everything needed to open a connection to the org's database over the tunnel. */
|
|
@@ -75,6 +75,16 @@ export interface WorkerAppDatabasePort extends WorkerAppMigrationRunner<Hyperdri
|
|
|
75
75
|
}): Promise<void>;
|
|
76
76
|
/** The text of a release asset of a wtfalch repo, or `null` when the release has no such asset. */
|
|
77
77
|
fetchReleaseAsset(url: string): Promise<string | null>;
|
|
78
|
+
/**
|
|
79
|
+
* After the runtime role work: the names among `tables` that `user` can still delete from or
|
|
80
|
+
* truncate (itself, through PUBLIC or through a role it belongs to), or that do not exist.
|
|
81
|
+
* Optional: without it a `noDelete` list is blocked at plan time.
|
|
82
|
+
*/
|
|
83
|
+
deletableTables?(input: {
|
|
84
|
+
owner: HyperdriveOrigin;
|
|
85
|
+
user: string;
|
|
86
|
+
tables: string[];
|
|
87
|
+
}): Promise<string[]>;
|
|
78
88
|
}
|
|
79
89
|
/** The Hyperdrive config, `{name, id}` as JSON. No secret. */
|
|
80
90
|
export declare const workerAppHyperdriveScopeKey: (entryKey: string) => string;
|
|
@@ -130,6 +140,7 @@ export declare function applyWorkerAppDatabase(deps: WorkerAppDatabaseDeps, org:
|
|
|
130
140
|
artifactUrl: string;
|
|
131
141
|
database: {
|
|
132
142
|
appendOnly: string[];
|
|
143
|
+
noDelete?: string[];
|
|
133
144
|
grants?: string[];
|
|
134
145
|
migrationsSha256?: string;
|
|
135
146
|
};
|
|
@@ -145,6 +145,19 @@ export async function applyWorkerAppDatabase(deps, org, entry, scriptName, want,
|
|
|
145
145
|
await context.persistScopeIds();
|
|
146
146
|
},
|
|
147
147
|
});
|
|
148
|
+
// 5a. `noDelete`: read the role's rights back, so a port that dropped the list cannot pass.
|
|
149
|
+
const noDelete = want.database.noDelete ?? [];
|
|
150
|
+
if (noDelete.length > 0) {
|
|
151
|
+
const read = port.deletableTables;
|
|
152
|
+
if (!read) {
|
|
153
|
+
throw new Error(`${at}: the host's database port cannot read the role's rights back (no deletableTables)`);
|
|
154
|
+
}
|
|
155
|
+
const open = await call("check the runtime role's rights", () => read.call(port, { owner: ownerOrigin, user: runtimeUser, tables: [...noDelete] }));
|
|
156
|
+
const named = noDelete.filter((table) => open.includes(table));
|
|
157
|
+
if (named.length > 0) {
|
|
158
|
+
throw new Error(`${at}: the runtime role can still delete from ${named.join(', ')} (or the table is missing); the Worker was not deployed`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
148
161
|
// 5b. `uses.audit`: the sealing role, apart from the runtime role, after the audit tables exist.
|
|
149
162
|
if (entry.uses?.audit === true && port.ensureSealerRole) {
|
|
150
163
|
await ensureAuditSealer(deps, org, entry, owner.database, ownerOrigin, at, call, context, put);
|
|
@@ -67,6 +67,10 @@ export declare const WORKER_APP_AUTH_KEY_SECRET = "AUTH_APP_KEY";
|
|
|
67
67
|
* and name are hashed: a held ref is at most 128 characters.
|
|
68
68
|
*/
|
|
69
69
|
export declare const workerAppSecretRef: (orgId: string, entryKey: string, name: string) => string;
|
|
70
|
+
/** The version recorded for a `heldSecret` value: a short hash, so a changed value is delivered again. Never the value. */
|
|
71
|
+
export declare const heldSecretVersion: (value: string) => string;
|
|
72
|
+
/** `<entry key>/<NAME>` of a `heldSecret`: how the plan's readiness names it. */
|
|
73
|
+
export declare const heldSecretId: (entryKey: string, name: string) => string;
|
|
70
74
|
/**
|
|
71
75
|
* The Cloudflare calls a Worker app needs. Edge 0.13.0 has none of the Durable
|
|
72
76
|
* Object, migration or container calls, so no adapter ships with this port yet
|
|
@@ -97,6 +101,11 @@ export interface WorkerAppPort {
|
|
|
97
101
|
value: string;
|
|
98
102
|
version: string;
|
|
99
103
|
}[]): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Sets the script's cron triggers to exactly `crons`; an empty list clears them. Safe to repeat.
|
|
106
|
+
* Edge's `client.platform.workers.setSchedules(name, { crons })`.
|
|
107
|
+
*/
|
|
108
|
+
setSchedules(name: string, crons: readonly string[]): Promise<void>;
|
|
100
109
|
/** A no-op when the secret is gone. */
|
|
101
110
|
deleteSecret(name: string, secretName: string): Promise<void>;
|
|
102
111
|
/** Creates or updates the container application of `className` on the script; returns its id. */
|
|
@@ -155,14 +164,21 @@ export interface WorkerAppWant {
|
|
|
155
164
|
/** `workerApp.database`: the Hyperdrive binding and the tables the runtime role may only append to, sorted. */
|
|
156
165
|
database?: {
|
|
157
166
|
appendOnly: string[];
|
|
167
|
+
noDelete?: string[];
|
|
158
168
|
grants?: string[];
|
|
159
169
|
migrationsSha256?: string;
|
|
160
170
|
};
|
|
161
|
-
/**
|
|
171
|
+
/** `workerApp.triggers.crons`, sorted. Absent when none, so a Worker without cron keeps the marker it already has. */
|
|
172
|
+
crons?: string[];
|
|
173
|
+
/**
|
|
174
|
+
* Secret names and where each comes from, sorted. A `heldSecret` is listed only while a value is held,
|
|
175
|
+
* with the version of that value (a hash, never the value); one with no value is not here.
|
|
176
|
+
*/
|
|
162
177
|
secrets: {
|
|
163
178
|
name: string;
|
|
164
|
-
source: 'generated' | 'keysCustody';
|
|
179
|
+
source: 'generated' | 'keysCustody' | 'heldSecret';
|
|
165
180
|
hostId?: string;
|
|
181
|
+
version?: string;
|
|
166
182
|
}[];
|
|
167
183
|
}
|
|
168
184
|
/** The zone apex the entry's route lives in. */
|
|
@@ -174,8 +190,11 @@ export declare const workerAppApex: (org: DeclaredOrg, entry?: HostingEntry) =>
|
|
|
174
190
|
export declare function wantedWorkerApp(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>, input: {
|
|
175
191
|
issuer: string | null;
|
|
176
192
|
keysUrl?: string;
|
|
193
|
+
/** `<entry key>/<NAME>` -> version of the value held now, for each `heldSecret` that has one. */
|
|
194
|
+
heldSecrets?: Readonly<Record<string, string>>;
|
|
177
195
|
}): {
|
|
178
196
|
want: WorkerAppWant;
|
|
197
|
+
unset: string[];
|
|
179
198
|
} | {
|
|
180
199
|
missing: string[];
|
|
181
200
|
};
|
|
@@ -201,6 +220,8 @@ export interface WorkerAppPlanInput {
|
|
|
201
220
|
canHoldSecrets: boolean;
|
|
202
221
|
/** The sign-in issuer for `authEnv`. */
|
|
203
222
|
issuer: string | null;
|
|
223
|
+
/** `<entry key>/<NAME>` -> version of the value held now, for each `heldSecret` that has one (names only, never a value). */
|
|
224
|
+
heldSecrets?: Readonly<Record<string, string>>;
|
|
204
225
|
/** The platform's Cloudflare account id: a container image must live in its registry. */
|
|
205
226
|
registryAccountId?: string;
|
|
206
227
|
/**
|
|
@@ -214,6 +235,8 @@ export interface WorkerAppPlanInput {
|
|
|
214
235
|
tunnelReady: boolean;
|
|
215
236
|
/** The executor has Access, tunnel, DNS and zone ports. */
|
|
216
237
|
accessReady: boolean;
|
|
238
|
+
/** The port has `deletableTables`, so a `noDelete` list can be proven after the role work. */
|
|
239
|
+
noDeleteCheck?: boolean;
|
|
217
240
|
};
|
|
218
241
|
}
|
|
219
242
|
/** Every entry key with a deployed or half-deployed Worker app recorded. */
|
|
@@ -245,6 +268,8 @@ export interface WorkerAppDeps {
|
|
|
245
268
|
};
|
|
246
269
|
/** `workerApp.database`: the Hyperdrive port and the route's Cloudflare ports. */
|
|
247
270
|
database?: WorkerAppDatabaseDeps;
|
|
271
|
+
/** Registers a value for redaction before anything else can see it. */
|
|
272
|
+
secret?: (value: string) => void;
|
|
248
273
|
/** Test hook: the timeout, in ms. */
|
|
249
274
|
timeoutMs?: number;
|
|
250
275
|
}
|