@substrat-run/control-plane-api 0.159.0 → 0.161.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/api.d.ts +33 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +674 -63
- package/dist/api.js.map +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -1
- package/dist/browser.js.map +1 -1
- package/dist/cf-observability.d.ts +30 -0
- package/dist/cf-observability.d.ts.map +1 -1
- package/dist/cf-observability.js +304 -99
- package/dist/cf-observability.js.map +1 -1
- package/dist/custom-hostnames.d.ts +30 -0
- package/dist/custom-hostnames.d.ts.map +1 -1
- package/dist/custom-hostnames.js +25 -2
- package/dist/custom-hostnames.js.map +1 -1
- package/dist/deploy.d.ts +3 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/observability.d.ts +29 -2
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +8 -0
- package/dist/observability.js.map +1 -1
- package/dist/platform-drain.d.ts +26 -2
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +297 -119
- package/dist/platform-drain.js.map +1 -1
- package/dist/platform-entry.generated.d.ts +2 -2
- package/dist/platform-entry.generated.d.ts.map +1 -1
- package/dist/platform-entry.generated.js +5 -5
- package/dist/platform-entry.generated.js.map +1 -1
- package/dist/platform-wire.d.ts +51 -0
- package/dist/platform-wire.d.ts.map +1 -0
- package/dist/platform-wire.js +163 -0
- package/dist/platform-wire.js.map +1 -0
- package/dist/scope-copy-backfill.d.ts.map +1 -1
- package/dist/scope-copy-backfill.js +22 -9
- package/dist/scope-copy-backfill.js.map +1 -1
- package/dist/scope-copy-cleanup.js +1 -1
- package/dist/scope-copy-cleanup.js.map +1 -1
- package/dist/staff-client.d.ts +38 -2
- package/dist/staff-client.d.ts.map +1 -1
- package/dist/staff-client.js +24 -1
- package/dist/staff-client.js.map +1 -1
- package/dist/vertical-client.d.ts +116 -4
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +366 -25
- package/dist/vertical-client.js.map +1 -1
- package/dist/wire-settings.d.ts +77 -0
- package/dist/wire-settings.d.ts.map +1 -0
- package/dist/wire-settings.js +155 -0
- package/dist/wire-settings.js.map +1 -0
- package/dist/wire-upload.d.ts +100 -0
- package/dist/wire-upload.d.ts.map +1 -0
- package/dist/wire-upload.js +141 -0
- package/dist/wire-upload.js.map +1 -0
- package/dist/wire-walk.d.ts +140 -0
- package/dist/wire-walk.d.ts.map +1 -0
- package/dist/wire-walk.js +339 -0
- package/dist/wire-walk.js.map +1 -0
- package/package.json +10 -10
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The uploader's half of RFC wire-signing step 2 (follow-on 3): what one upload gives the deployment
|
|
3
|
+
* it goes to, and the capability record of it (§5, §6, §7, Q8).
|
|
4
|
+
*
|
|
5
|
+
* A deployment is given both issuers' public key lists, its own dispatch name for the audience
|
|
6
|
+
* check and the state `dual`, all plain configuration, rewritten on every upload. Nothing is given
|
|
7
|
+
* until the control plane's `WIRE_KEYS_ROLLOUT` says so: unset or `off` gives no deployment lists,
|
|
8
|
+
* `canary:<vertical>` gives them to that one vertical's deployments only (the single canary the RFC
|
|
9
|
+
* puts first in each environment), and `fleet` to every deployment. A setting that cannot be read
|
|
10
|
+
* gives nothing. So with the setting unset, every upload is what it was before this existed.
|
|
11
|
+
*
|
|
12
|
+
* The lists are judged before they are given, by the function the platform's entry loads them with
|
|
13
|
+
* (`wireUploadSettings`), so a deployment is never given lists its verifier would refuse: the upload
|
|
14
|
+
* gives nothing instead, and says why. No code here can give a state other than `dual`: moving a
|
|
15
|
+
* deployment to `signed-only` is the flip, a later and separate step.
|
|
16
|
+
*
|
|
17
|
+
* Every upload has a record, given lists or not, written from what the uploader built: whether the
|
|
18
|
+
* bundle carries the platform's entry (which holds the verifier), the state, the audience and the key
|
|
19
|
+
* ids, or why nothing was given. The vertical never declares any of it.
|
|
20
|
+
*/
|
|
21
|
+
import { verticalSlug } from '@substrat-run/contracts';
|
|
22
|
+
import { wireUploadSettings, wireVerificationKeys, WIRE_ENVELOPE_VERSION, WIRE_UPLOAD_STATE } from '@substrat-run/contracts/wire-signing';
|
|
23
|
+
import { platformEntrySkipReason } from './platform-entry.js';
|
|
24
|
+
const CANARY = 'canary:';
|
|
25
|
+
/**
|
|
26
|
+
* The rollout setting's one reading. Unset, empty or `off` is off; `fleet` is every deployment;
|
|
27
|
+
* `canary:<vertical>` is one vertical, named as the registry names it. Anything else is refused
|
|
28
|
+
* with the reason, a second vertical in a canary included: the RFC's canary is one, before the rest.
|
|
29
|
+
*/
|
|
30
|
+
export function wireKeysRolloutOf(value) {
|
|
31
|
+
if (value === undefined || value === null)
|
|
32
|
+
return { kind: 'off' };
|
|
33
|
+
if (typeof value !== 'string')
|
|
34
|
+
return { refused: 'the rollout setting is not text' };
|
|
35
|
+
const text = value.trim();
|
|
36
|
+
if (text === '' || text === 'off')
|
|
37
|
+
return { kind: 'off' };
|
|
38
|
+
if (text === 'fleet')
|
|
39
|
+
return { kind: 'fleet' };
|
|
40
|
+
if (!text.startsWith(CANARY))
|
|
41
|
+
return { refused: 'the rollout setting is `off`, `canary:<vertical>` or `fleet`' };
|
|
42
|
+
const vertical = text.slice(CANARY.length).trim();
|
|
43
|
+
if (/[,\s]/.test(vertical))
|
|
44
|
+
return { refused: 'a canary names one vertical' };
|
|
45
|
+
if (!verticalSlug.safeParse(vertical).success)
|
|
46
|
+
return { refused: 'a canary names a vertical by its registry slug' };
|
|
47
|
+
return { kind: 'canary', vertical };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What the upload of `bundle` to the script `deploymentRef`, a deployment of `vertical`, gives it
|
|
51
|
+
* under `settings`, and the record of it. Gives nothing while the rollout is off or leaves the
|
|
52
|
+
* vertical out, while the setting cannot be read, or while the lists would not load.
|
|
53
|
+
*/
|
|
54
|
+
export async function wireUploadFor(settings, upload) {
|
|
55
|
+
return wireUploadGiven(settings, { vertical: upload.vertical, deploymentRef: upload.deploymentRef, entry: wireEntryOf(upload.bundle) });
|
|
56
|
+
}
|
|
57
|
+
/** Whether a bundle carries the platform's entry, which holds the verifier, as a record states it. */
|
|
58
|
+
export function wireEntryOf(bundle) {
|
|
59
|
+
const skipped = platformEntrySkipReason(bundle) ?? null;
|
|
60
|
+
return { envelope: skipped === null ? WIRE_ENVELOPE_VERSION : null, entrySkipped: skipped };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* `wireUploadFor` from a bundle's entry state already known (`wireEntryOf`, or a record of the
|
|
64
|
+
* script's upload): what the settings give the script now, and the record of it. The fleet walk's
|
|
65
|
+
* reading of the same rule, so a walked script and an uploaded one are given the same.
|
|
66
|
+
*/
|
|
67
|
+
export async function wireUploadGiven(settings, upload) {
|
|
68
|
+
const { vertical, deploymentRef } = upload;
|
|
69
|
+
const entry = { envelope: upload.entry.envelope, entrySkipped: upload.entry.entrySkipped };
|
|
70
|
+
const nothing = (withheld, refusal = null) => ({
|
|
71
|
+
vertical,
|
|
72
|
+
deploymentRef,
|
|
73
|
+
bindings: [],
|
|
74
|
+
record: { ...entry, state: 'legacy-only', audience: null, routerKids: [], platformKids: [], withheld },
|
|
75
|
+
refusal,
|
|
76
|
+
});
|
|
77
|
+
const rollout = wireKeysRolloutOf(settings?.rollout);
|
|
78
|
+
if ('refused' in rollout)
|
|
79
|
+
return nothing('rollout-refused', rollout.refused);
|
|
80
|
+
if (rollout.kind === 'off')
|
|
81
|
+
return nothing('unconfigured');
|
|
82
|
+
if (rollout.kind === 'canary' && rollout.vertical !== vertical)
|
|
83
|
+
return nothing('not-in-rollout');
|
|
84
|
+
try {
|
|
85
|
+
const given = await wireUploadSettings({
|
|
86
|
+
routerKeys: settings?.routerKeys ?? '',
|
|
87
|
+
platformKeys: settings?.platformKeys ?? '',
|
|
88
|
+
audience: deploymentRef,
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
vertical,
|
|
92
|
+
deploymentRef,
|
|
93
|
+
bindings: given.settings.map((setting) => ({ type: 'plain_text', ...setting })),
|
|
94
|
+
record: { ...entry, state: WIRE_UPLOAD_STATE, audience: deploymentRef, routerKids: given.routerKids, platformKids: given.platformKids, withheld: null },
|
|
95
|
+
refusal: null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return nothing('lists-refused', await refusedList(settings, error));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether the settings themselves are refused: the rollout setting does not parse, or, where it gives
|
|
104
|
+
* lists to anything, the lists do not load. `null` when they are usable, `off` included. An upload under
|
|
105
|
+
* refused settings gives nothing and says why; the fleet walk refuses the whole pass instead, since
|
|
106
|
+
* giving nothing to every script would withdraw the lists fleet-wide. The decision is the uploader's
|
|
107
|
+
* own (`wireUploadGiven`, asked for a deployment inside the rollout), so the two cannot drift; the
|
|
108
|
+
* reason names the setting, never a key.
|
|
109
|
+
*/
|
|
110
|
+
export async function wireSettingsRefusal(settings) {
|
|
111
|
+
const rollout = wireKeysRolloutOf(settings?.rollout);
|
|
112
|
+
const vertical = 'refused' in rollout || rollout.kind !== 'canary' ? 'wire-settings-check' : rollout.vertical;
|
|
113
|
+
const given = await wireUploadGiven(settings, { vertical, deploymentRef: 'wire-settings-check', entry: { envelope: 1, entrySkipped: null } });
|
|
114
|
+
const withheld = given.record.withheld;
|
|
115
|
+
return given.refusal !== null && (withheld === 'rollout-refused' || withheld === 'lists-refused') ? { withheld, reason: given.refusal } : null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Why the lists were refused, naming the setting to fix: each list is loaded alone, and the first
|
|
119
|
+
* that fails is named with its problem. When each loads alone (a key in both, or an audience the
|
|
120
|
+
* encoder would not carry), the refusal is the one given. The parser's messages are fixed text
|
|
121
|
+
* naming a problem, never a key.
|
|
122
|
+
*/
|
|
123
|
+
async function refusedList(settings, error) {
|
|
124
|
+
const problem = (e) => (e instanceof Error ? e.message : 'the list did not load').replace(/^a store public key/, 'a public key');
|
|
125
|
+
for (const [name, router, platform] of [
|
|
126
|
+
['WIRE_ROUTER_PUBLIC_KEYS', settings?.routerKeys, undefined],
|
|
127
|
+
['WIRE_PLATFORM_PUBLIC_KEYS', undefined, settings?.platformKeys],
|
|
128
|
+
]) {
|
|
129
|
+
const list = router ?? platform;
|
|
130
|
+
if (!list?.trim())
|
|
131
|
+
return `${name} lists no key`;
|
|
132
|
+
try {
|
|
133
|
+
await wireVerificationKeys({ router, platform });
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
return `${name}: ${problem(e)}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return problem(error);
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=wire-upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-upload.js","sourceRoot":"","sources":["../src/wire-upload.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fleet walk (RFC wire-signing follow-on 4): bring every deployed script's key lists, audience
|
|
3
|
+
* and state to what the control plane gives now, so that a key listed, rotated, dropped or withdrawn
|
|
4
|
+
* reaches every script, not only the ones an upload happens to reach. A vertical's serving script is
|
|
5
|
+
* uploaded again at each promote, but a version's own script (its archive script, which a preview is
|
|
6
|
+
* routed to) is uploaded once, at its push, and nothing else ever rewrites it.
|
|
7
|
+
*
|
|
8
|
+
* A person runs it, a pass at a time, and it reports before it changes anything:
|
|
9
|
+
*
|
|
10
|
+
* - **Dry run by default.** Without `apply` a pass only reads: the settings write is unreachable, no
|
|
11
|
+
* operational failure is filed and no hold is released.
|
|
12
|
+
* - **Bounded and resumable.** A pass walks at most `limit` scripts in a fixed order (verticals by
|
|
13
|
+
* slug; each one's serving script, then its versions' scripts by id) and answers the cursor the next
|
|
14
|
+
* pass starts from, `null` when the fleet is done. A vertical deleted since it was listed, or named by
|
|
15
|
+
* a saved cursor, is passed over.
|
|
16
|
+
* - **Only scripts it can attribute.** The inventory is the registry's: a vertical's serving script and
|
|
17
|
+
* each of its versions' scripts. A script the registry does not name is never touched, nor is one
|
|
18
|
+
* whose name is the platform's own.
|
|
19
|
+
* - **No call it does not need.** A script whose record already says it was given what the settings give
|
|
20
|
+
* now is not read. A script with no record, which the settings give nothing, is not read either: in an
|
|
21
|
+
* environment where the rollout was never set, a pass makes no call to the runtime at all. A
|
|
22
|
+
* **confirming** pass (`confirm`) reads every recorded script instead, trusting no record; a held
|
|
23
|
+
* script, and one with no record that nothing is given or that is skipped, it leaves unread as well.
|
|
24
|
+
* - **Only a script that verifies is given lists by it.** A script with no record is read back first, on
|
|
25
|
+
* every pass that reaches it (no verdict is kept between passes: pushing it again gives it a record);
|
|
26
|
+
* one that runs without the platform's entry is skipped (`no-entry`), never recorded as verifying.
|
|
27
|
+
* - **One script's failure is that script's.** It is reported, recorded as an operational failure by
|
|
28
|
+
* reason, and the pass goes on.
|
|
29
|
+
* - **A script it cannot vouch for is held.** When the read after an update differs, or the serve moved
|
|
30
|
+
* under it, the script's record is marked held, with the binding names that differed, and later passes
|
|
31
|
+
* leave it alone until a person names it in `release`, or an upload of it writes a fresh record.
|
|
32
|
+
*
|
|
33
|
+
* Each script it changes writes its capability record and that record's admin row in one directory
|
|
34
|
+
* transaction (`recordWireAuth`), and each pass writes one `walkWireAuth` row. The settings write is
|
|
35
|
+
* `wire-settings.ts`. Do not run the walk while a push or promote of the same vertical is in flight,
|
|
36
|
+
* and end with a confirming pass.
|
|
37
|
+
*/
|
|
38
|
+
import { type PlatformActorId } from '@substrat-run/contracts';
|
|
39
|
+
import type { HostAdmin } from '@substrat-run/kernel';
|
|
40
|
+
import type { FetchVerticalModulesFn } from './deploy.js';
|
|
41
|
+
import { type WireSettingsApi } from './wire-settings.js';
|
|
42
|
+
import { type WireUploadSettings } from './wire-upload.js';
|
|
43
|
+
/** A pass walks this many scripts when it names no limit. */
|
|
44
|
+
export declare const WIRE_WALK_LIMIT_DEFAULT = 25;
|
|
45
|
+
/** A pass walks at most this many scripts. */
|
|
46
|
+
export declare const WIRE_WALK_LIMIT_MAX = 50;
|
|
47
|
+
/** A pass releases at most this many held scripts. */
|
|
48
|
+
export declare const WIRE_WALK_RELEASE_MAX = 50;
|
|
49
|
+
/**
|
|
50
|
+
* Where a pass starts: inside a vertical, after its serving script (`after` empty) or after one of its
|
|
51
|
+
* versions (`after` that version's id, whatever its spelling: the publish route takes any id).
|
|
52
|
+
*/
|
|
53
|
+
export interface WireWalkCursor {
|
|
54
|
+
readonly vertical: string;
|
|
55
|
+
readonly after: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A cursor as a pass answers it, or `null` for one that is not one. Opaque to a caller. A slug never
|
|
59
|
+
* holds a `|`, so the first one ends it, and everything after it is the position.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseWireWalkCursor(text: string): WireWalkCursor | null;
|
|
62
|
+
/** What a pass did, or in a dry run would do, with one script. */
|
|
63
|
+
export type WireWalkAction =
|
|
64
|
+
/** Its record says it was given what the settings give now: not read. */
|
|
65
|
+
'current'
|
|
66
|
+
/** No record, and the settings give it nothing: not read. */
|
|
67
|
+
| 'unrecorded'
|
|
68
|
+
/** Held by an earlier pass and not released: not read. */
|
|
69
|
+
| 'held'
|
|
70
|
+
/** Not given lists, for `reason`: `no-entry`, a script with no record that runs without the platform's entry. */
|
|
71
|
+
| 'skipped'
|
|
72
|
+
/** Read: it already carries what the settings give now. Applied, its record is refreshed. */
|
|
73
|
+
| 'unchanged'
|
|
74
|
+
/** A dry run: these settings would change. */
|
|
75
|
+
| 'would-change'
|
|
76
|
+
/** Updated, verified and recorded. */
|
|
77
|
+
| 'updated'
|
|
78
|
+
/** Not done, for `reason`; held as well when the reason says the script could not be vouched for. */
|
|
79
|
+
| 'failed';
|
|
80
|
+
export interface WireWalkScript {
|
|
81
|
+
readonly deploymentRef: string;
|
|
82
|
+
readonly vertical: string;
|
|
83
|
+
readonly versionId: string | null;
|
|
84
|
+
readonly kind: 'serving' | 'version';
|
|
85
|
+
readonly action: WireWalkAction;
|
|
86
|
+
/** The settings that differ (`would-change`, `updated`), by name. */
|
|
87
|
+
readonly changes?: readonly string[];
|
|
88
|
+
/**
|
|
89
|
+
* Why it failed (`concurrent-change`, `serve-in-flight`, `verify`, `entry-unknown`, `settings`,
|
|
90
|
+
* `settings-refused` or `record`), or was skipped (`no-entry`).
|
|
91
|
+
*/
|
|
92
|
+
readonly reason?: string;
|
|
93
|
+
/** For `verify`: the binding names that differed. Never a value. */
|
|
94
|
+
readonly bindings?: readonly string[];
|
|
95
|
+
/** For `verify`: how many more bindings differed than `bindings` names (it keeps the first `WALK_HOLD_BINDINGS_MAX`). */
|
|
96
|
+
readonly more?: number;
|
|
97
|
+
/** Whether the pass left the script held. */
|
|
98
|
+
readonly held?: true;
|
|
99
|
+
/** Whether it was held and a person released it for this pass. */
|
|
100
|
+
readonly released?: true;
|
|
101
|
+
}
|
|
102
|
+
export interface WireWalkPass {
|
|
103
|
+
readonly apply: boolean;
|
|
104
|
+
/** The cursor the pass started from, `null` for the start of the fleet. */
|
|
105
|
+
readonly from: string | null;
|
|
106
|
+
/** Where the next pass starts, or `null` when this one reached the end of the fleet. */
|
|
107
|
+
readonly next: string | null;
|
|
108
|
+
readonly scripts: readonly WireWalkScript[];
|
|
109
|
+
}
|
|
110
|
+
export interface WireWalkDeps {
|
|
111
|
+
readonly admin: HostAdmin;
|
|
112
|
+
readonly actor: PlatformActorId;
|
|
113
|
+
readonly settings: WireUploadSettings | undefined;
|
|
114
|
+
readonly api: WireSettingsApi;
|
|
115
|
+
/** Reads a script's modules back, to learn whether a script with no record carries the platform's entry. */
|
|
116
|
+
readonly fetchModules?: FetchVerticalModulesFn;
|
|
117
|
+
/** Records a script's failure as an operational failure: names, reasons and binding names only. */
|
|
118
|
+
readonly fail: (entry: {
|
|
119
|
+
readonly vertical: string;
|
|
120
|
+
readonly version: string | null;
|
|
121
|
+
readonly stage: string;
|
|
122
|
+
readonly message: string;
|
|
123
|
+
}) => void;
|
|
124
|
+
readonly now?: () => Date;
|
|
125
|
+
}
|
|
126
|
+
/** Run one pass. Never throws for one script's failure; throws only for a directory read that fails. */
|
|
127
|
+
export declare function runWireWalk(deps: WireWalkDeps, input: {
|
|
128
|
+
readonly apply: boolean;
|
|
129
|
+
readonly cursor: WireWalkCursor | null;
|
|
130
|
+
readonly limit: number;
|
|
131
|
+
readonly release: readonly string[];
|
|
132
|
+
/** A confirming pass: read every recorded script, never trusting its record (no `current`). */
|
|
133
|
+
readonly confirm?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Where each script's result is put as soon as it is known: the caller's, so a pass that ends in a
|
|
136
|
+
* throw still says what it did. Defaults to a fresh list.
|
|
137
|
+
*/
|
|
138
|
+
readonly sink?: WireWalkScript[];
|
|
139
|
+
}): Promise<WireWalkPass>;
|
|
140
|
+
//# sourceMappingURL=wire-walk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-walk.d.ts","sourceRoot":"","sources":["../src/wire-walk.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fleet walk (RFC wire-signing follow-on 4): bring every deployed script's key lists, audience
|
|
3
|
+
* and state to what the control plane gives now, so that a key listed, rotated, dropped or withdrawn
|
|
4
|
+
* reaches every script, not only the ones an upload happens to reach. A vertical's serving script is
|
|
5
|
+
* uploaded again at each promote, but a version's own script (its archive script, which a preview is
|
|
6
|
+
* routed to) is uploaded once, at its push, and nothing else ever rewrites it.
|
|
7
|
+
*
|
|
8
|
+
* A person runs it, a pass at a time, and it reports before it changes anything:
|
|
9
|
+
*
|
|
10
|
+
* - **Dry run by default.** Without `apply` a pass only reads: the settings write is unreachable, no
|
|
11
|
+
* operational failure is filed and no hold is released.
|
|
12
|
+
* - **Bounded and resumable.** A pass walks at most `limit` scripts in a fixed order (verticals by
|
|
13
|
+
* slug; each one's serving script, then its versions' scripts by id) and answers the cursor the next
|
|
14
|
+
* pass starts from, `null` when the fleet is done. A vertical deleted since it was listed, or named by
|
|
15
|
+
* a saved cursor, is passed over.
|
|
16
|
+
* - **Only scripts it can attribute.** The inventory is the registry's: a vertical's serving script and
|
|
17
|
+
* each of its versions' scripts. A script the registry does not name is never touched, nor is one
|
|
18
|
+
* whose name is the platform's own.
|
|
19
|
+
* - **No call it does not need.** A script whose record already says it was given what the settings give
|
|
20
|
+
* now is not read. A script with no record, which the settings give nothing, is not read either: in an
|
|
21
|
+
* environment where the rollout was never set, a pass makes no call to the runtime at all. A
|
|
22
|
+
* **confirming** pass (`confirm`) reads every recorded script instead, trusting no record; a held
|
|
23
|
+
* script, and one with no record that nothing is given or that is skipped, it leaves unread as well.
|
|
24
|
+
* - **Only a script that verifies is given lists by it.** A script with no record is read back first, on
|
|
25
|
+
* every pass that reaches it (no verdict is kept between passes: pushing it again gives it a record);
|
|
26
|
+
* one that runs without the platform's entry is skipped (`no-entry`), never recorded as verifying.
|
|
27
|
+
* - **One script's failure is that script's.** It is reported, recorded as an operational failure by
|
|
28
|
+
* reason, and the pass goes on.
|
|
29
|
+
* - **A script it cannot vouch for is held.** When the read after an update differs, or the serve moved
|
|
30
|
+
* under it, the script's record is marked held, with the binding names that differed, and later passes
|
|
31
|
+
* leave it alone until a person names it in `release`, or an upload of it writes a fresh record.
|
|
32
|
+
*
|
|
33
|
+
* Each script it changes writes its capability record and that record's admin row in one directory
|
|
34
|
+
* transaction (`recordWireAuth`), and each pass writes one `walkWireAuth` row. The settings write is
|
|
35
|
+
* `wire-settings.ts`. Do not run the walk while a push or promote of the same vertical is in flight,
|
|
36
|
+
* and end with a confirming pass.
|
|
37
|
+
*/
|
|
38
|
+
import { errorCodeOf, instant, LIST_PAGE_MAX, PLATFORM_SCRIPT_NAMES, storedDeployManifest, verticalSlug as verticalSlugSchema, WALK_HOLD_BINDINGS_MAX, } from '@substrat-run/contracts';
|
|
39
|
+
import { PLATFORM_ENTRY_MODULE } from './platform-entry.js';
|
|
40
|
+
import { applyWireSettings, CONCURRENT_RETRIES } from './wire-settings.js';
|
|
41
|
+
import { wireEntryOf, wireUploadGiven } from './wire-upload.js';
|
|
42
|
+
/** A pass walks this many scripts when it names no limit. */
|
|
43
|
+
export const WIRE_WALK_LIMIT_DEFAULT = 25;
|
|
44
|
+
/** A pass walks at most this many scripts. */
|
|
45
|
+
export const WIRE_WALK_LIMIT_MAX = 50;
|
|
46
|
+
/** A pass releases at most this many held scripts. */
|
|
47
|
+
export const WIRE_WALK_RELEASE_MAX = 50;
|
|
48
|
+
const PAGE = 50;
|
|
49
|
+
const GONE = Symbol('gone');
|
|
50
|
+
/** The cursor's position inside a vertical just after its serving script: nothing after the `|`. */
|
|
51
|
+
const AFTER_SERVING = '';
|
|
52
|
+
/**
|
|
53
|
+
* A cursor as a pass answers it, or `null` for one that is not one. Opaque to a caller. A slug never
|
|
54
|
+
* holds a `|`, so the first one ends it, and everything after it is the position.
|
|
55
|
+
*/
|
|
56
|
+
export function parseWireWalkCursor(text) {
|
|
57
|
+
const at = text.indexOf('|');
|
|
58
|
+
if (at <= 0)
|
|
59
|
+
return null;
|
|
60
|
+
const vertical = text.slice(0, at);
|
|
61
|
+
if (!verticalSlugSchema.safeParse(vertical).success)
|
|
62
|
+
return null;
|
|
63
|
+
return { vertical, after: text.slice(at + 1) };
|
|
64
|
+
}
|
|
65
|
+
const cursorText = (vertical, after) => `${vertical}|${after}`;
|
|
66
|
+
/** The registry's scripts in walk order, from `from` on: each vertical's serving script, then its versions' scripts. */
|
|
67
|
+
async function* inventory(admin, actor, from) {
|
|
68
|
+
/** A directory read of one vertical, or `gone` when the vertical no longer exists (deleted since it was listed, or named by a saved cursor). */
|
|
69
|
+
const ofVertical = async (read) => {
|
|
70
|
+
try {
|
|
71
|
+
return await read();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (errorCodeOf(error) === 'not_found')
|
|
75
|
+
return GONE;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
async function* scriptsOf(vertical, after) {
|
|
80
|
+
const serving = await ofVertical(() => admin.verticalServing(actor, vertical));
|
|
81
|
+
if (serving === GONE)
|
|
82
|
+
return;
|
|
83
|
+
const servingRef = serving && !PLATFORM_SCRIPT_NAMES.has(serving.ref) ? serving.ref : null;
|
|
84
|
+
if (after === null && servingRef !== null) {
|
|
85
|
+
yield { vertical, deploymentRef: servingRef, versionId: serving.versionId, kind: 'serving', cursor: cursorText(vertical, AFTER_SERVING) };
|
|
86
|
+
}
|
|
87
|
+
let cursor = after !== null && after !== AFTER_SERVING ? after : undefined;
|
|
88
|
+
for (;;) {
|
|
89
|
+
const versions = await ofVertical(() => admin.listVersions(actor, vertical, { limit: PAGE, ...(cursor ? { cursor } : {}) }));
|
|
90
|
+
if (versions === GONE)
|
|
91
|
+
return;
|
|
92
|
+
for (const v of versions) {
|
|
93
|
+
const ref = v.deploymentRef;
|
|
94
|
+
if (ref === null || ref === servingRef || PLATFORM_SCRIPT_NAMES.has(ref))
|
|
95
|
+
continue;
|
|
96
|
+
yield { vertical, deploymentRef: ref, versionId: v.id, kind: 'version', cursor: cursorText(vertical, v.id) };
|
|
97
|
+
}
|
|
98
|
+
if (versions.length < PAGE)
|
|
99
|
+
return;
|
|
100
|
+
cursor = versions.at(-1).id;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
let slugCursor;
|
|
104
|
+
if (from) {
|
|
105
|
+
yield* scriptsOf(from.vertical, from.after);
|
|
106
|
+
slugCursor = from.vertical;
|
|
107
|
+
}
|
|
108
|
+
for (;;) {
|
|
109
|
+
const verticals = await admin.listVerticals(actor, { limit: PAGE, ...(slugCursor ? { cursor: slugCursor } : {}) });
|
|
110
|
+
for (const v of verticals)
|
|
111
|
+
yield* scriptsOf(v.slug, null);
|
|
112
|
+
if (verticals.length < PAGE)
|
|
113
|
+
return;
|
|
114
|
+
slugCursor = verticals.at(-1).slug;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Whether a record already holds what a given record says, in what a script carries: the state, the audience and the key ids. */
|
|
118
|
+
function carriesAlready(stored, given) {
|
|
119
|
+
const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
120
|
+
return (stored.state === given.state &&
|
|
121
|
+
stored.audience === given.audience &&
|
|
122
|
+
same(stored.routerKids, given.routerKids) &&
|
|
123
|
+
same(stored.platformKids, given.platformKids));
|
|
124
|
+
}
|
|
125
|
+
/** Run one pass. Never throws for one script's failure; throws only for a directory read that fails. */
|
|
126
|
+
export async function runWireWalk(deps, input) {
|
|
127
|
+
const { admin, actor, settings, api } = deps;
|
|
128
|
+
const now = deps.now ?? (() => new Date());
|
|
129
|
+
const release = new Set(input.apply ? input.release : []);
|
|
130
|
+
const recordsBy = async (vertical) => {
|
|
131
|
+
const held = new Map();
|
|
132
|
+
let cursor;
|
|
133
|
+
for (;;) {
|
|
134
|
+
const page = await admin.listWireAuth(actor, { reach: { fleet: true }, verticalSlug: vertical, limit: LIST_PAGE_MAX, ...(cursor ? { cursor } : {}) });
|
|
135
|
+
for (const r of page)
|
|
136
|
+
held.set(r.deploymentRef, r);
|
|
137
|
+
if (page.length < LIST_PAGE_MAX)
|
|
138
|
+
return held;
|
|
139
|
+
cursor = page.at(-1).deploymentRef;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const records = new Map();
|
|
143
|
+
const recordsOf = async (vertical) => {
|
|
144
|
+
let held = records.get(vertical);
|
|
145
|
+
if (held)
|
|
146
|
+
return held;
|
|
147
|
+
held = await recordsBy(vertical);
|
|
148
|
+
records.clear(); // one vertical at a time: the walk moves through them in order
|
|
149
|
+
records.set(vertical, held);
|
|
150
|
+
return held;
|
|
151
|
+
};
|
|
152
|
+
const record = (script, rec, versionId = script.versionId) => admin.recordWireAuth(actor, { deploymentRef: script.deploymentRef, verticalSlug: script.vertical, versionId, record: rec, recordedAt: now().toISOString() });
|
|
153
|
+
/**
|
|
154
|
+
* Whether the script carries the platform's entry, which holds the verifier, read back from the
|
|
155
|
+
* script itself: its modules must hold the entry module, and its own bundle must be one the entry
|
|
156
|
+
* wraps. `no-manifest` when it cannot be told; `no-entry` when the script runs without one.
|
|
157
|
+
*/
|
|
158
|
+
const entryOf = async (script) => {
|
|
159
|
+
if (!deps.fetchModules || script.versionId === null)
|
|
160
|
+
return 'no-manifest';
|
|
161
|
+
const manifestJson = await admin.versionManifest(actor, script.vertical, script.versionId);
|
|
162
|
+
if (!manifestJson)
|
|
163
|
+
return 'no-manifest';
|
|
164
|
+
const manifest = storedDeployManifest.parse(JSON.parse(manifestJson));
|
|
165
|
+
const modules = await deps.fetchModules(script.deploymentRef);
|
|
166
|
+
if (!modules.some((m) => m.name === PLATFORM_ENTRY_MODULE))
|
|
167
|
+
return 'no-entry';
|
|
168
|
+
const entry = wireEntryOf({ entry: manifest.entry, modules });
|
|
169
|
+
return entry.envelope === null ? 'no-entry' : entry;
|
|
170
|
+
};
|
|
171
|
+
const walkOne = async (script) => {
|
|
172
|
+
const stored = (await recordsOf(script.vertical)).get(script.deploymentRef);
|
|
173
|
+
const wasHeld = stored?.record.walkHold !== undefined;
|
|
174
|
+
const base = { deploymentRef: script.deploymentRef, vertical: script.vertical, versionId: script.versionId, kind: script.kind };
|
|
175
|
+
if (wasHeld && !release.has(script.deploymentRef))
|
|
176
|
+
return { ...base, action: 'held' };
|
|
177
|
+
/** A failure: reported, and recorded as an operational failure when the pass applies (a dry run attempts nothing). */
|
|
178
|
+
const failed = (reason, message, extra = {}) => {
|
|
179
|
+
if (input.apply) {
|
|
180
|
+
deps.fail({ vertical: script.vertical, version: script.versionId, stage: reason, message: `the wire walk did not bring '${script.deploymentRef}' current: ${message}` });
|
|
181
|
+
}
|
|
182
|
+
return { ...base, action: 'failed', reason, ...extra };
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Mark the script held: a script the walk wrote and could not confirm is never left without one.
|
|
186
|
+
* It holds the record as it stands now, read again (an upload's record written during the pass
|
|
187
|
+
* included, which the hold keeps and marks), or, with none, a record that claims no lists
|
|
188
|
+
* (`unverified`). It keeps the first `WALK_HOLD_BINDINGS_MAX` binding names and counts the rest.
|
|
189
|
+
* A write that fails is tried once more; answers whether the hold was written, and the failure the
|
|
190
|
+
* walk reports says so when it was not.
|
|
191
|
+
*/
|
|
192
|
+
const hold = async (entry, reason, bindings) => {
|
|
193
|
+
const once = async () => {
|
|
194
|
+
const latest = (await recordsBy(script.vertical)).get(script.deploymentRef);
|
|
195
|
+
const known = latest
|
|
196
|
+
? (({ walkHold: _previous, ...rest }) => rest)(latest.record)
|
|
197
|
+
: { ...entry, state: 'legacy-only', audience: null, routerKids: [], platformKids: [], withheld: 'unverified' };
|
|
198
|
+
const kept = bindings.slice(0, WALK_HOLD_BINDINGS_MAX);
|
|
199
|
+
const more = bindings.length - kept.length;
|
|
200
|
+
const walkHold = { reason, bindings: [...kept], ...(more > 0 ? { more } : {}), at: instant.parse(now().toISOString()) };
|
|
201
|
+
// Under the version the record it marks names: an upload's record keeps its upload's version.
|
|
202
|
+
await record(script, { ...known, walkHold }, latest ? latest.versionId : script.versionId);
|
|
203
|
+
};
|
|
204
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
205
|
+
try {
|
|
206
|
+
await once();
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Tried once more, then reported.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
215
|
+
/** Binding names for a person to read: the first `WALK_HOLD_BINDINGS_MAX`, and how many more. */
|
|
216
|
+
const named = (bindings) => bindings.length > WALK_HOLD_BINDINGS_MAX
|
|
217
|
+
? `${bindings.slice(0, WALK_HOLD_BINDINGS_MAX).join(', ')} and ${bindings.length - WALK_HOLD_BINDINGS_MAX} more`
|
|
218
|
+
: bindings.join(', ');
|
|
219
|
+
const holdNote = (held) => (held ? 'held until a person looks' : 'and the hold itself could not be written: look at it now');
|
|
220
|
+
try {
|
|
221
|
+
let entry = stored ? { envelope: stored.record.envelope, entrySkipped: stored.record.entrySkipped } : null;
|
|
222
|
+
if (!entry) {
|
|
223
|
+
// No record: an upload from before records existed, or one whose record was not written. The
|
|
224
|
+
// settings decide whether it needs reading at all; whether it carries the entry needs its bundle.
|
|
225
|
+
const probe = await wireUploadGiven(settings, { vertical: script.vertical, deploymentRef: script.deploymentRef, entry: { envelope: 1, entrySkipped: null } });
|
|
226
|
+
if (probe.bindings.length === 0)
|
|
227
|
+
return { ...base, action: 'unrecorded' };
|
|
228
|
+
const read = await entryOf(script);
|
|
229
|
+
if (read === 'no-manifest')
|
|
230
|
+
return failed('entry-unknown', 'no record, and no stored manifest to read its entry from: upload it again');
|
|
231
|
+
// It runs without the platform's entry, so nothing in it verifies: it is not given lists, nor
|
|
232
|
+
// recorded as verifying. Pushing it again wraps it.
|
|
233
|
+
if (read === 'no-entry')
|
|
234
|
+
return { ...base, action: 'skipped', reason: 'no-entry' };
|
|
235
|
+
entry = read;
|
|
236
|
+
}
|
|
237
|
+
const given = await wireUploadGiven(settings, { vertical: script.vertical, deploymentRef: script.deploymentRef, entry });
|
|
238
|
+
// Refused settings give nothing, and nothing is not a withdrawal to write: the route refuses such a
|
|
239
|
+
// pass before it starts, and a script never acts on one here either.
|
|
240
|
+
// The route refuses a pass under refused settings before it starts, so what is refused here is
|
|
241
|
+
// this script alone (its name is not one the lists can be given as an audience).
|
|
242
|
+
if (given.refusal !== null)
|
|
243
|
+
return failed('settings-refused', `the lists are refused for this script (${given.refusal}): nothing was written`);
|
|
244
|
+
if (!input.confirm && stored && !stored.record.walkHold && carriesAlready(stored.record, given.record))
|
|
245
|
+
return { ...base, action: 'current' };
|
|
246
|
+
let outcome;
|
|
247
|
+
try {
|
|
248
|
+
outcome = await applyWireSettings(api, script.deploymentRef, given.bindings.map((b) => ({ name: b.name, text: b.text })), {
|
|
249
|
+
apply: input.apply,
|
|
250
|
+
...(script.kind === 'serving' && script.versionId !== null
|
|
251
|
+
? {
|
|
252
|
+
servedVersion: script.versionId,
|
|
253
|
+
servedVersionAfter: async () => (await admin.verticalServing(actor, script.vertical))?.versionId ?? null,
|
|
254
|
+
}
|
|
255
|
+
: {}),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
// The settings API's own messages name the script and a status, never a value.
|
|
260
|
+
return failed('settings', error instanceof Error ? error.message : 'the settings call failed');
|
|
261
|
+
}
|
|
262
|
+
switch (outcome.outcome) {
|
|
263
|
+
case 'would-change':
|
|
264
|
+
return { ...base, action: 'would-change', changes: outcome.changes };
|
|
265
|
+
case 'unchanged':
|
|
266
|
+
case 'updated': {
|
|
267
|
+
if (input.apply) {
|
|
268
|
+
try {
|
|
269
|
+
await record(script, given.record);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return failed('record', 'its settings are current, but its record was not written');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return outcome.outcome === 'updated' ? { ...base, action: 'updated', changes: outcome.changes } : { ...base, action: 'unchanged' };
|
|
276
|
+
}
|
|
277
|
+
case 'concurrent-change':
|
|
278
|
+
return failed('concurrent-change', `its bindings changed between reads ${CONCURRENT_RETRIES} times; nothing was written`);
|
|
279
|
+
case 'serve-in-flight': {
|
|
280
|
+
if (!outcome.written)
|
|
281
|
+
return failed('serve-in-flight', 'it does not run the version the directory says it serves; nothing was written: promote again, then walk');
|
|
282
|
+
const held = await hold(entry, 'serve-in-flight', []);
|
|
283
|
+
return failed('serve-in-flight', `the served version moved while it was updated, ${holdNote(held)}; ${held ? 'promote again, then release it' : 'promote again'}`, held ? { held: true } : {});
|
|
284
|
+
}
|
|
285
|
+
case 'verify': {
|
|
286
|
+
const held = await hold(entry, 'verify', outcome.bindings);
|
|
287
|
+
return failed('verify', `after its update, these bindings read differently: ${named(outcome.bindings)}; ${holdNote(held)}`, {
|
|
288
|
+
bindings: outcome.bindings.slice(0, WALK_HOLD_BINDINGS_MAX),
|
|
289
|
+
...(outcome.bindings.length > WALK_HOLD_BINDINGS_MAX ? { more: outcome.bindings.length - WALK_HOLD_BINDINGS_MAX } : {}),
|
|
290
|
+
...(held ? { held: true } : {}),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// A read for this one script (its stored manifest, its modules) failed. Its text is not echoed:
|
|
297
|
+
// nothing here vouches that it carries no value.
|
|
298
|
+
return failed('entry-unknown', 'its stored manifest or its modules could not be read');
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* One script, and whether a person's release of it took: only a script that was held and came out
|
|
303
|
+
* current (its fresh record, with no hold, written) is `released`; one that failed again stays held.
|
|
304
|
+
*/
|
|
305
|
+
const walkReleasing = async (script) => {
|
|
306
|
+
const result = await walkOne(script);
|
|
307
|
+
const wasHeld = (await recordsOf(script.vertical)).get(script.deploymentRef)?.record.walkHold !== undefined;
|
|
308
|
+
const current = result.action === 'updated' || result.action === 'unchanged';
|
|
309
|
+
return wasHeld && release.has(script.deploymentRef) && current ? { ...result, released: true } : result;
|
|
310
|
+
};
|
|
311
|
+
const scripts = input.sink ?? [];
|
|
312
|
+
let next = null;
|
|
313
|
+
const fleet = inventory(admin, actor, input.cursor);
|
|
314
|
+
try {
|
|
315
|
+
let step = await fleet.next();
|
|
316
|
+
while (!step.done) {
|
|
317
|
+
const script = step.value;
|
|
318
|
+
scripts.push(await walkReleasing(script));
|
|
319
|
+
if (scripts.length >= input.limit) {
|
|
320
|
+
// Looked one ahead, so the pass that ends on the fleet's last script answers `null`. A look
|
|
321
|
+
// that fails does not undo a pass that is done: the next one starts after its last script.
|
|
322
|
+
try {
|
|
323
|
+
step = await fleet.next();
|
|
324
|
+
next = step.done ? null : script.cursor;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
next = script.cursor;
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
step = await fleet.next();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
await fleet.return(undefined).catch(() => undefined);
|
|
336
|
+
}
|
|
337
|
+
return { apply: input.apply, from: input.cursor ? cursorText(input.cursor.vertical, input.cursor.after) : null, next, scripts };
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=wire-walk.js.map
|