@everfur/cli 0.1.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/LICENSE +21 -0
- package/README.md +114 -0
- package/contract/contract-public.json +1335 -0
- package/dist/chunk-67H43MI6.js +1822 -0
- package/dist/chunk-KQZ6ZXL7.js +3994 -0
- package/dist/everfur-mcp.d.ts +2 -0
- package/dist/everfur-mcp.js +369 -0
- package/dist/everfur.d.ts +2 -0
- package/dist/everfur.js +27 -0
- package/dist/index.d.ts +2511 -0
- package/dist/index.js +22 -0
- package/package.json +85 -0
- package/schema/contract-public.v1.schema.json +36 -0
- package/schema/diagnosis.v1.schema.json +166 -0
- package/schema/error-explanation.v1.schema.json +65 -0
- package/schema/plan.v1.schema.json +244 -0
- package/schema/sandbox-create.v1.schema.json +74 -0
- package/schema/verification.v1.schema.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CREDENTIAL RESOLUTION, and the rules that make "the CLI never prints a secret" a structural
|
|
3
|
+
* property rather than a promise.
|
|
4
|
+
*
|
|
5
|
+
* 1. A credential comes from a NAMED ENVIRONMENT VARIABLE or from nowhere. This package never
|
|
6
|
+
* reads a `.env` file, never walks up looking for one, and has no dotenv dependency: a repo
|
|
7
|
+
* `.env` is a file a partner commits by accident, and a tool that reads one teaches them
|
|
8
|
+
* that committing it is fine. The names are documented in README.md and printed by
|
|
9
|
+
* `--help`, so an operator is told what to export rather than where a file should live.
|
|
10
|
+
* 2. The resolved value is wrapped. `toString` and `toJSON` both return `[redacted]`, so the
|
|
11
|
+
* two ways a value normally escapes (string interpolation into a message, and
|
|
12
|
+
* `JSON.stringify` of an outcome) yield the marker instead of the secret. Only `reveal()`
|
|
13
|
+
* returns the real bytes, and `tests/unit/secret-never-printed.test.ts` asserts that the
|
|
14
|
+
* HTTP client is the only module that calls it.
|
|
15
|
+
* 3. Shape is checked before the value is used. A truncated paste is reported by NAME as a
|
|
16
|
+
* malformed credential, which is a better answer than the opaque 401 the server gives for
|
|
17
|
+
* every credential failure alike (`sessions_router._auth_rejected`).
|
|
18
|
+
*/
|
|
19
|
+
declare const REDACTED = "[redacted]";
|
|
20
|
+
interface Credential {
|
|
21
|
+
/** The variable this came from. Safe to print; it is a name, not a value. */
|
|
22
|
+
readonly envVar: string;
|
|
23
|
+
toString(): string;
|
|
24
|
+
toJSON(): string;
|
|
25
|
+
/** The real value. Called by `src/http/client.ts` and nowhere else. */
|
|
26
|
+
reveal(): string;
|
|
27
|
+
}
|
|
28
|
+
interface CredentialSpec {
|
|
29
|
+
readonly envVar: string;
|
|
30
|
+
/** Required literal prefix, or null when the value is opaque (a bearer token). */
|
|
31
|
+
readonly prefix: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Additional accepted prefixes, beyond `prefix`.
|
|
34
|
+
*
|
|
35
|
+
* A publishable key is `pk_live_` on a production deployment and `pk_test_` on a sandbox one, and the
|
|
36
|
+
* CLI cannot tell which host it is pointed at before it makes a call. Hardcoding one of them would make
|
|
37
|
+
* the CLI reject exactly the sandbox credentials the platform now issues, which is the opposite of what
|
|
38
|
+
* a test-mode tool should do. The SERVER is the authority: it accepts one prefix per deployment and
|
|
39
|
+
* refuses the other on the fast-fail path, so accepting both here narrows nothing.
|
|
40
|
+
*/
|
|
41
|
+
readonly alsoAccepts?: readonly string[];
|
|
42
|
+
/** Exact expected length when the prefix is known, or null. */
|
|
43
|
+
readonly length: number | null;
|
|
44
|
+
readonly summary: string;
|
|
45
|
+
}
|
|
46
|
+
/** The publishable key. Public by construction, but still never echoed back to the operator. */
|
|
47
|
+
declare const PARTNER_KEY: CredentialSpec;
|
|
48
|
+
/**
|
|
49
|
+
* The server-side half of the session mint. The variable name is the same constant `plan`
|
|
50
|
+
* already names in its configure step; see `src/commands/plan/planner.ts`.
|
|
51
|
+
*/
|
|
52
|
+
declare const PARTNER_SECRET_KEY: CredentialSpec;
|
|
53
|
+
/** An already minted device bearer, for diagnosing the signed path without a mint. */
|
|
54
|
+
declare const SESSION_TOKEN: CredentialSpec;
|
|
55
|
+
/** The operator bearer for the admin routes. */
|
|
56
|
+
declare const ADMIN_TOKEN: CredentialSpec;
|
|
57
|
+
declare const CREDENTIAL_SPECS: readonly CredentialSpec[];
|
|
58
|
+
type CredentialStatus = 'present' | 'absent' | 'malformed';
|
|
59
|
+
interface ResolvedCredential {
|
|
60
|
+
readonly spec: CredentialSpec;
|
|
61
|
+
readonly status: CredentialStatus;
|
|
62
|
+
/** Populated only when `status` is `present`. */
|
|
63
|
+
readonly credential: Credential | null;
|
|
64
|
+
/** Why the value was rejected. Names the variable and the expected shape, never the value. */
|
|
65
|
+
readonly reason: string | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Wrap a value that did NOT come from the environment, for example a session token the CLI just
|
|
69
|
+
* minted. It is exactly as unprintable as an environment credential, which is the point: a
|
|
70
|
+
* token that arrives over the wire is no less a secret than one an operator exported, and
|
|
71
|
+
* `verify` has to carry one from step one to step two without it reaching a report.
|
|
72
|
+
*
|
|
73
|
+
* `envVar` names the origin rather than a variable, because there is no variable.
|
|
74
|
+
*/
|
|
75
|
+
declare function ephemeralCredential(origin: string, value: string): Credential;
|
|
76
|
+
/**
|
|
77
|
+
* Wrap a credential so a scheme is prepended when it is finally revealed, for example turning a
|
|
78
|
+
* session token into `Bearer <token>`. The value is never interpolated here: the concatenation
|
|
79
|
+
* happens inside `reveal`, which only the HTTP client calls, so a prefixed credential is exactly
|
|
80
|
+
* as unprintable as an unprefixed one.
|
|
81
|
+
*/
|
|
82
|
+
declare function withPrefix(credential: Credential, prefix: string): Credential;
|
|
83
|
+
/**
|
|
84
|
+
* Read one credential out of the injected environment. Absent and malformed are DIFFERENT
|
|
85
|
+
* answers, exactly as an absent and an invalid flag are different to `plan`: absent means the
|
|
86
|
+
* operator has not set it up yet, malformed means they did and it is wrong.
|
|
87
|
+
*/
|
|
88
|
+
declare function resolveCredential(env: Readonly<Record<string, string | undefined>>, spec: CredentialSpec): ResolvedCredential;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The filesystem seam. Detection and contract loading are pure functions over this interface,
|
|
92
|
+
* which is the whole reason they can be tested against fixtures without a temp directory
|
|
93
|
+
* dance and without a process spawn.
|
|
94
|
+
*
|
|
95
|
+
* Synchronous by design. The CLI reads a handful of small manifests and then exits; async
|
|
96
|
+
* would buy nothing and would make every detection helper a promise for no reason.
|
|
97
|
+
*
|
|
98
|
+
* `readFile` returns null rather than throwing for a missing or unreadable path. Detection
|
|
99
|
+
* probes many paths that are legitimately absent, so absence is a value, not an exception.
|
|
100
|
+
*/
|
|
101
|
+
interface FileSystemPort {
|
|
102
|
+
/** UTF-8 contents, or null when the path is missing, unreadable, or a directory. */
|
|
103
|
+
readFile(path: string): string | null;
|
|
104
|
+
/** True when the path exists, whatever its type. */
|
|
105
|
+
exists(path: string): boolean;
|
|
106
|
+
/** True when the path exists and is a directory. */
|
|
107
|
+
isDirectory(path: string): boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* THE NETWORK SEAM. One interface, injected exactly like `FileSystemPort`, which is what lets
|
|
112
|
+
* every command that talks to the API be exercised in a unit test with a scripted transport and
|
|
113
|
+
* no socket. Nothing above this file knows that `fetch` exists.
|
|
114
|
+
*
|
|
115
|
+
* The request and response are plain data. In particular a `HttpRequest` is NEVER carried into
|
|
116
|
+
* a `CommandOutcome`: it holds the credential headers, and an outcome is a thing the formatter
|
|
117
|
+
* prints. Failures carry the method, the path, and the request id, never the request object.
|
|
118
|
+
*/
|
|
119
|
+
type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
|
120
|
+
/** Methods that may be retried. A retry of anything else could mint a second credential. */
|
|
121
|
+
declare const IDEMPOTENT_METHODS: readonly HttpMethod[];
|
|
122
|
+
interface HttpRequest {
|
|
123
|
+
readonly method: HttpMethod;
|
|
124
|
+
readonly url: string;
|
|
125
|
+
/** Header names as written. Values may include a credential; see the note above. */
|
|
126
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
127
|
+
readonly body: string | null;
|
|
128
|
+
/** Hard ceiling for this single attempt, in milliseconds. Enforced by the adapter. */
|
|
129
|
+
readonly timeoutMs: number;
|
|
130
|
+
}
|
|
131
|
+
interface HttpResponse {
|
|
132
|
+
readonly status: number;
|
|
133
|
+
/** Header names LOWERCASED by the adapter, so callers never guess at casing. */
|
|
134
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
135
|
+
readonly body: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The only exception a transport may throw. Anything else escaping an adapter is a bug in that
|
|
139
|
+
* adapter; `createApiClient` still catches it and reports it as a network failure rather than
|
|
140
|
+
* letting a raw stack reach the user.
|
|
141
|
+
*/
|
|
142
|
+
declare class TransportError extends Error {
|
|
143
|
+
readonly kind: 'timeout' | 'network';
|
|
144
|
+
constructor(kind: 'timeout' | 'network', message: string);
|
|
145
|
+
}
|
|
146
|
+
interface TransportPort {
|
|
147
|
+
send(request: HttpRequest): Promise<HttpResponse>;
|
|
148
|
+
}
|
|
149
|
+
/** True when a method may be retried after a transport fault or a retryable server answer. */
|
|
150
|
+
declare function isIdempotent(method: HttpMethod): boolean;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The time seam. Only `verify` needs it, to space out its entitlement poll, and only because a
|
|
154
|
+
* real sleep in a unit test is a slow test that eventually becomes a flaky one.
|
|
155
|
+
*
|
|
156
|
+
* Deliberately minimal: no `now()`. Every timestamp this CLI emits comes from the server
|
|
157
|
+
* (`evaluated_at`, `expires_at`), so there is no clock reading to inject and no output that
|
|
158
|
+
* changes between two runs of the same command.
|
|
159
|
+
*/
|
|
160
|
+
interface ClockPort {
|
|
161
|
+
sleep(milliseconds: number): Promise<void>;
|
|
162
|
+
}
|
|
163
|
+
declare const systemClock: ClockPort;
|
|
164
|
+
/** A clock that never waits and records what it was asked to wait for. For tests. */
|
|
165
|
+
declare function createRecordingClock(): ClockPort & {
|
|
166
|
+
readonly waits: readonly number[];
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A command declares its flags as data. Binding is a pure function from that declaration plus
|
|
171
|
+
* a ParsedArgv to either typed values or a list of usage errors, so "is --identity a known
|
|
172
|
+
* flag on this command" is answered by the registry, not by each command re-implementing it.
|
|
173
|
+
*/
|
|
174
|
+
interface FlagSpec {
|
|
175
|
+
readonly name: string;
|
|
176
|
+
readonly kind: 'string' | 'boolean';
|
|
177
|
+
readonly summary: string;
|
|
178
|
+
/** Rendered in help as `--name <placeholder>`. Value flags only. */
|
|
179
|
+
readonly placeholder?: string;
|
|
180
|
+
readonly required?: boolean;
|
|
181
|
+
}
|
|
182
|
+
interface BoundFlags {
|
|
183
|
+
/** Value of a string flag, or null when absent. */
|
|
184
|
+
string(name: string): string | null;
|
|
185
|
+
/** Value of a boolean flag. Absent means false. */
|
|
186
|
+
boolean(name: string): boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The frozen exit-code table. Every command settles to one of these; nothing else may
|
|
191
|
+
* reach the process. Codes are additive: a future command may claim a new code, but an
|
|
192
|
+
* existing code never changes meaning, because CI pipelines branch on them.
|
|
193
|
+
*
|
|
194
|
+
* The split between UNDERSPECIFIED (3) and INVALID_CONTRACT (4) / DIGEST_MISMATCH (5) is
|
|
195
|
+
* deliberate. 3 means "the CLI declined to answer because you did not tell it enough";
|
|
196
|
+
* it is a refusal, not a failure, and a wrapper script is expected to prompt and retry.
|
|
197
|
+
* 4 and 5 mean an artifact on disk is wrong and a human has to look at it.
|
|
198
|
+
*/
|
|
199
|
+
declare const ExitCode: Readonly<{
|
|
200
|
+
/** The command produced a complete answer. */
|
|
201
|
+
readonly OK: 0;
|
|
202
|
+
/** An unexpected internal failure. A bug in the CLI, or an unreadable filesystem. */
|
|
203
|
+
readonly INTERNAL_ERROR: 1;
|
|
204
|
+
/** The invocation was malformed: unknown command, unknown flag, missing flag value. */
|
|
205
|
+
readonly USAGE_ERROR: 2;
|
|
206
|
+
/** The CLI refused to guess. Required facts were missing; see `missingFacts`. */
|
|
207
|
+
readonly UNDERSPECIFIED: 3;
|
|
208
|
+
/**
|
|
209
|
+
* A NAMED ARTIFACT was missing, unparseable, or failed shape validation against its JSON
|
|
210
|
+
* Schema. All three mean the same thing to a caller: a file this command was pointed at
|
|
211
|
+
* cannot be used, and a human has to look at that file.
|
|
212
|
+
*
|
|
213
|
+
* The wording was tightened from "failed shape validation" because `file_not_found` and
|
|
214
|
+
* `invalid_json` have always mapped here too, and a code whose documented meaning is
|
|
215
|
+
* narrower than its behaviour is a code CI branches on wrongly. What does NOT belong here is
|
|
216
|
+
* a bad INVOCATION: `--cwd /nope` is a usage error (2), because nothing was loaded.
|
|
217
|
+
*/
|
|
218
|
+
readonly INVALID_CONTRACT: 4;
|
|
219
|
+
/** A loaded artifact was well shaped but its digest did not match its content. */
|
|
220
|
+
readonly DIGEST_MISMATCH: 5;
|
|
221
|
+
/**
|
|
222
|
+
* The API could not be reached, did not answer in time, or answered with a typed error.
|
|
223
|
+
* The invocation was fine and no local artifact is wrong: the far side is the problem, so a
|
|
224
|
+
* CI wrapper branching on this retries or pages rather than sending someone to edit a file.
|
|
225
|
+
*/
|
|
226
|
+
readonly REMOTE_FAILURE: 6;
|
|
227
|
+
/**
|
|
228
|
+
* `verify` completed and at least one step FAILED. Distinct from 6 because the wire worked:
|
|
229
|
+
* the CLI got answers and one of them was wrong, which is a report to read rather than an
|
|
230
|
+
* outage to retry. A run with no failures but a skipped step settles at 3, not here, because
|
|
231
|
+
* a skip means the operator did not supply enough to exercise that step.
|
|
232
|
+
*/
|
|
233
|
+
readonly VERIFICATION_FAILED: 7;
|
|
234
|
+
/**
|
|
235
|
+
* `sandbox create` provisioned a key, and the server offered no claim URL for its secret.
|
|
236
|
+
* The CLI will not print a raw secret, so it discarded the one it was handed unread. The key
|
|
237
|
+
* exists and nobody can use it; the human projection prints the revoke command.
|
|
238
|
+
*/
|
|
239
|
+
readonly SECRET_UNCLAIMABLE: 8;
|
|
240
|
+
/**
|
|
241
|
+
* `plan` was told everything it needed and NOTHING the caller asked for can be built: every
|
|
242
|
+
* capability named resolved to a BLOCKED terminal in the contract.
|
|
243
|
+
*
|
|
244
|
+
* Distinct from 3 because supplying another flag will not help, and distinct from 0 because
|
|
245
|
+
* no plan was produced. A CI wrapper branching on 3 prompts for more input, which would loop
|
|
246
|
+
* forever here; one branching on 9 reads the reason and the request path out of the document
|
|
247
|
+
* and escalates to a human, which is the only thing that resolves it.
|
|
248
|
+
*/
|
|
249
|
+
readonly BLOCKED: 9;
|
|
250
|
+
}>;
|
|
251
|
+
type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The JSON value lattice. Command outcomes carry a `JsonValue`, never an arbitrary object,
|
|
255
|
+
* so the machine projection is provably serialisable and provably free of class instances,
|
|
256
|
+
* functions, and `undefined` holes that would silently vanish through JSON.stringify.
|
|
257
|
+
*/
|
|
258
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
259
|
+
type JsonValue = JsonPrimitive | readonly JsonValue[] | {
|
|
260
|
+
readonly [key: string]: JsonValue;
|
|
261
|
+
};
|
|
262
|
+
type JsonObject = {
|
|
263
|
+
readonly [key: string]: JsonValue;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The single shared result type every command settles to. There is exactly one of these and
|
|
268
|
+
* every future command returns it, which is what lets the formatter, the bin shim, and the
|
|
269
|
+
* exit-code table stay unaware of what any individual command does.
|
|
270
|
+
*
|
|
271
|
+
* Both projections are always populated. A command never chooses a rendering: it produces the
|
|
272
|
+
* machine document and the human text, and `formatOutcome` decides which one reaches which
|
|
273
|
+
* stream. That is why command logic never imports a stream and never calls console.
|
|
274
|
+
*/
|
|
275
|
+
interface CommandOutcome {
|
|
276
|
+
readonly exitCode: ExitCode;
|
|
277
|
+
/** The machine projection. Emitted verbatim under `--json`, on stdout, success or failure. */
|
|
278
|
+
readonly json: JsonObject;
|
|
279
|
+
/** The human projection. Already wrapped; the formatter adds only a trailing newline. */
|
|
280
|
+
readonly text: string;
|
|
281
|
+
/**
|
|
282
|
+
* Where the human projection goes. Diagnostics belong on stderr so that `everfur plan > out`
|
|
283
|
+
* still shows the operator why nothing useful landed in the file.
|
|
284
|
+
*/
|
|
285
|
+
readonly stream: 'stdout' | 'stderr';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Everything a command is allowed to touch, injected. There is no ambient `process.cwd()`,
|
|
290
|
+
* no ambient `process.env`, and no module-level filesystem. That is what lets every command
|
|
291
|
+
* be exercised in a unit test with a fixture directory and no child process.
|
|
292
|
+
*/
|
|
293
|
+
interface CommandContext {
|
|
294
|
+
readonly fs: FileSystemPort;
|
|
295
|
+
/** Absolute working directory. Resolved once, in the bin shim. */
|
|
296
|
+
readonly cwd: string;
|
|
297
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
298
|
+
/** The CLI's own version, stamped into every document it emits. */
|
|
299
|
+
readonly version: string;
|
|
300
|
+
/**
|
|
301
|
+
* The network seam. Injected for the same reason the filesystem is: a test scripts it and no
|
|
302
|
+
* socket opens. `src/bin` supplies the one real adapter.
|
|
303
|
+
*/
|
|
304
|
+
readonly transport: TransportPort;
|
|
305
|
+
/** Waiting, injected so a poll loop in a test costs nothing. */
|
|
306
|
+
readonly clock: ClockPort;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* A positional argument. Declared as data beside the flags so `--help` documents it and the
|
|
310
|
+
* orchestrator can enforce arity, rather than each command counting its own leftovers.
|
|
311
|
+
*/
|
|
312
|
+
interface PositionalSpec {
|
|
313
|
+
readonly name: string;
|
|
314
|
+
readonly summary: string;
|
|
315
|
+
readonly required: boolean;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A command is data plus one pure-ish function. `run` may read the filesystem through the
|
|
319
|
+
* injected port and nothing else: no writing, no network, no stream, no exit.
|
|
320
|
+
*
|
|
321
|
+
* Adding a future command is adding one of these to the registry array. No other file changes,
|
|
322
|
+
* which is the property this scaffold exists to establish before there are five more commands.
|
|
323
|
+
*/
|
|
324
|
+
interface Command {
|
|
325
|
+
/** Command path tokens, for example `["contract", "validate"]`. Matched in order. */
|
|
326
|
+
readonly path: readonly string[];
|
|
327
|
+
readonly summary: string;
|
|
328
|
+
/** Usage line shown by `--help`, without the leading `everfur `. */
|
|
329
|
+
readonly usage: string;
|
|
330
|
+
readonly flags: readonly FlagSpec[];
|
|
331
|
+
/** Ordered positional arguments this command accepts. Absent means it accepts none. */
|
|
332
|
+
readonly positionals?: readonly PositionalSpec[];
|
|
333
|
+
/**
|
|
334
|
+
* The credentials this command can actually use, declared as data for the same reason the
|
|
335
|
+
* flags are: `--help` renders what the command declares, so a credential a command reads is a
|
|
336
|
+
* credential its own help names. Absent means the command touches none, which is the truth
|
|
337
|
+
* for `plan`, `contract validate`, and `explain-error`.
|
|
338
|
+
*
|
|
339
|
+
* This exists because per-command help used to render only the flags. A partner told to run
|
|
340
|
+
* one command had no way to learn from the tool that they must export EVERFUR_PARTNER_KEY;
|
|
341
|
+
* they found out from a `credential_missing` failure at runtime instead.
|
|
342
|
+
*/
|
|
343
|
+
readonly credentials?: readonly CredentialSpec[];
|
|
344
|
+
run(context: CommandContext, flags: BoundFlags, positionals: readonly string[]): Promise<CommandOutcome>;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* THE single output formatter. Every byte the CLI writes is produced here, from a
|
|
349
|
+
* CommandOutcome, which is why no command needs a stream and why `--json` needed no per-command
|
|
350
|
+
* support.
|
|
351
|
+
*
|
|
352
|
+
* The rule, chosen once so scripts can rely on it:
|
|
353
|
+
*
|
|
354
|
+
* - with `--json`, the machine document goes to stdout ALWAYS, success or failure. A caller
|
|
355
|
+
* that has to parse a failure differently from a success ends up parsing stderr, and then
|
|
356
|
+
* the contract is prose.
|
|
357
|
+
* - without `--json`, human text goes to stdout on success and stderr on failure, so
|
|
358
|
+
* `everfur plan > plan.txt` still shows the operator why the file is empty.
|
|
359
|
+
* - exactly one trailing newline on each non-empty stream, and nothing at all on the other.
|
|
360
|
+
*/
|
|
361
|
+
interface FormattedOutput {
|
|
362
|
+
readonly stdout: string;
|
|
363
|
+
readonly stderr: string;
|
|
364
|
+
readonly exitCode: number;
|
|
365
|
+
}
|
|
366
|
+
interface FormatOptions {
|
|
367
|
+
readonly json: boolean;
|
|
368
|
+
}
|
|
369
|
+
declare function formatOutcome(outcome: CommandOutcome, options: FormatOptions): FormattedOutput;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The orchestrator: argv in, formatted output plus exit code out. It writes nothing and exits
|
|
373
|
+
* nothing, so the whole CLI including its exit codes can be asserted in a unit test without a
|
|
374
|
+
* child process. `src/bin/everfur.ts` is the only file that turns this return value into an
|
|
375
|
+
* actual process effect.
|
|
376
|
+
*
|
|
377
|
+
* `--json` is read from the raw parse rather than from bound flags, because the formatter has
|
|
378
|
+
* to honour it even for an invocation whose flags failed to bind. A usage error under `--json`
|
|
379
|
+
* still emits a JSON error document.
|
|
380
|
+
*/
|
|
381
|
+
interface RunOptions {
|
|
382
|
+
readonly argv: readonly string[];
|
|
383
|
+
readonly context: CommandContext;
|
|
384
|
+
readonly commands?: readonly Command[];
|
|
385
|
+
}
|
|
386
|
+
declare function runCli(options: RunOptions): Promise<FormattedOutput>;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* THE REGISTRY. Exactly the commands this release ships, and nothing else.
|
|
390
|
+
*
|
|
391
|
+
* `login` and `test message` are deliberately ABSENT. They are described in ROADMAP.md and have
|
|
392
|
+
* no code here, registered or otherwise. A stub that resolves to "not implemented" is worse than
|
|
393
|
+
* a missing command: it teaches a partner that the command exists and then wastes their time,
|
|
394
|
+
* and it invites a half-implementation to accrete behind it. `everfur login` prints
|
|
395
|
+
* "unknown command", which is the truth.
|
|
396
|
+
*
|
|
397
|
+
* Longer paths are matched first so `contract validate` and `sandbox create` win over any future
|
|
398
|
+
* bare `contract` or `sandbox`.
|
|
399
|
+
*/
|
|
400
|
+
declare const COMMANDS: readonly Command[];
|
|
401
|
+
interface CommandMatch {
|
|
402
|
+
readonly command: Command;
|
|
403
|
+
/** Argv path tokens the command consumed. */
|
|
404
|
+
readonly consumed: number;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Resolve a command path prefix to a command. Returns null when nothing matches, which the
|
|
408
|
+
* caller renders as a usage error rather than falling back to a default command.
|
|
409
|
+
*/
|
|
410
|
+
declare function matchCommand(path: readonly string[], commands?: readonly Command[]): CommandMatch | null;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The one real transport adapter, and the only place in the package that opens a socket.
|
|
414
|
+
* Everything it can go wrong with collapses to a `TransportError` with a kind, so the client
|
|
415
|
+
* above it branches on two values rather than on the shape of an undici error.
|
|
416
|
+
*
|
|
417
|
+
* `AbortSignal.timeout` bounds the whole attempt including the body read, which a
|
|
418
|
+
* `setTimeout` around the fetch promise would not: a response whose headers arrive quickly and
|
|
419
|
+
* whose body never finishes would hang forever under the naive version.
|
|
420
|
+
*
|
|
421
|
+
* No redirect following, because a redirect off the configured base URL would carry the
|
|
422
|
+
* partner's credential headers to whatever host answered.
|
|
423
|
+
*/
|
|
424
|
+
declare const fetchTransport: TransportPort;
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* WHERE THE CLI TALKS TO, resolved once and passed down. Two sources, in this order:
|
|
428
|
+
*
|
|
429
|
+
* 1. `--base-url <url>`, which wins.
|
|
430
|
+
* 2. `EVERFUR_API_BASE_URL`, the configured default for a shell that works against one
|
|
431
|
+
* environment all day.
|
|
432
|
+
*
|
|
433
|
+
* There is deliberately NO built-in default. A partner CLI that silently points at production
|
|
434
|
+
* when nothing is configured is one typo away from an operator diagnosing staging and reading a
|
|
435
|
+
* production answer, and the failure is invisible because both answers look plausible. Absent
|
|
436
|
+
* configuration is a usage error that names both ways to fix it.
|
|
437
|
+
*
|
|
438
|
+
* `API_PREFIX` is appended by this module rather than being part of what the operator types,
|
|
439
|
+
* because it is a deployment fact of the API and not a choice. A base URL that already ends in
|
|
440
|
+
* the prefix is accepted as-is, so both spellings work and neither doubles up.
|
|
441
|
+
*/
|
|
442
|
+
declare const BASE_URL_ENV_VAR = "EVERFUR_API_BASE_URL";
|
|
443
|
+
/** `app/main.py` mounts every router used here at `settings.api_prefix`. */
|
|
444
|
+
declare const API_PREFIX = "/api/v1";
|
|
445
|
+
interface ResolvedEndpoint {
|
|
446
|
+
/** Normalised, without a trailing slash. Safe to print: it is never a credential. */
|
|
447
|
+
readonly baseUrl: string;
|
|
448
|
+
/** True when the operator's base URL already carried `API_PREFIX`. */
|
|
449
|
+
readonly prefixIncluded: boolean;
|
|
450
|
+
readonly source: 'flag' | 'env';
|
|
451
|
+
}
|
|
452
|
+
type EndpointResolution = {
|
|
453
|
+
readonly ok: true;
|
|
454
|
+
readonly endpoint: ResolvedEndpoint;
|
|
455
|
+
} | {
|
|
456
|
+
readonly ok: false;
|
|
457
|
+
readonly errors: readonly string[];
|
|
458
|
+
};
|
|
459
|
+
declare function resolveEndpoint(flagValue: string | null, env: Readonly<Record<string, string | undefined>>): EndpointResolution;
|
|
460
|
+
/**
|
|
461
|
+
* Join an API-relative path (one starting `/widget/v1/...` or `/admin/...`) onto the resolved
|
|
462
|
+
* base, inserting `API_PREFIX` exactly once.
|
|
463
|
+
*/
|
|
464
|
+
declare function apiUrl(endpoint: ResolvedEndpoint, path: string): string;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The JSON Schema files are the single source of truth for validation, and the TypeScript types
|
|
468
|
+
* beside them are the compile-time view of the same contract. Nothing hand-rolls a second
|
|
469
|
+
* validator: `tests/contract` runs real emitted output through these compiled schemas, so a
|
|
470
|
+
* type that drifts from its schema fails the suite rather than shipping.
|
|
471
|
+
*
|
|
472
|
+
* The schemas are imported, not read from disk at runtime, so a globally installed CLI cannot
|
|
473
|
+
* end up validating against a schema file that a different version left lying around.
|
|
474
|
+
*
|
|
475
|
+
* Compilation is lazy and memoised: a `plan` run that never emits JSON should not pay for ajv
|
|
476
|
+
* compiling a schema it will not use.
|
|
477
|
+
*/
|
|
478
|
+
interface SchemaViolation {
|
|
479
|
+
/** JSON Pointer into the validated document, for example `/steps/2/phase`. */
|
|
480
|
+
readonly path: string;
|
|
481
|
+
readonly message: string;
|
|
482
|
+
}
|
|
483
|
+
interface SchemaValidation {
|
|
484
|
+
readonly valid: boolean;
|
|
485
|
+
readonly violations: readonly SchemaViolation[];
|
|
486
|
+
}
|
|
487
|
+
/** Validate a document against the committed plan contract. */
|
|
488
|
+
declare function validatePlanDocument(document: unknown): SchemaValidation;
|
|
489
|
+
/** Validate a document against the contract-public envelope contract. */
|
|
490
|
+
declare function validateContractPublicEnvelope(document: unknown): SchemaValidation;
|
|
491
|
+
/** Validate a document against the committed `everfur diagnose --json` contract. */
|
|
492
|
+
declare function validateDiagnosisDocument(document: unknown): SchemaValidation;
|
|
493
|
+
/** Validate a document against the committed `everfur sandbox create --json` contract. */
|
|
494
|
+
declare function validateSandboxCreateDocument(document: unknown): SchemaValidation;
|
|
495
|
+
/** Validate a document against the committed `everfur verify --json` contract. */
|
|
496
|
+
declare function validateVerificationDocument(document: unknown): SchemaValidation;
|
|
497
|
+
/** Validate a document against the committed `everfur explain-error --json` contract. */
|
|
498
|
+
declare function validateErrorExplanationDocument(document: unknown): SchemaValidation;
|
|
499
|
+
/** The raw schema objects, exported so tests and downstream tools can assert against them. */
|
|
500
|
+
declare const SCHEMAS: Readonly<{
|
|
501
|
+
plan: {
|
|
502
|
+
$schema: string;
|
|
503
|
+
$id: string;
|
|
504
|
+
title: string;
|
|
505
|
+
description: string;
|
|
506
|
+
type: string;
|
|
507
|
+
required: string[];
|
|
508
|
+
properties: {
|
|
509
|
+
schemaVersion: {
|
|
510
|
+
const: number;
|
|
511
|
+
};
|
|
512
|
+
kind: {
|
|
513
|
+
const: string;
|
|
514
|
+
};
|
|
515
|
+
generatedBy: {
|
|
516
|
+
type: string;
|
|
517
|
+
required: string[];
|
|
518
|
+
properties: {
|
|
519
|
+
name: {
|
|
520
|
+
type: string;
|
|
521
|
+
minLength: number;
|
|
522
|
+
};
|
|
523
|
+
version: {
|
|
524
|
+
type: string;
|
|
525
|
+
minLength: number;
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
status: {
|
|
530
|
+
enum: string[];
|
|
531
|
+
};
|
|
532
|
+
detected: {
|
|
533
|
+
$ref: string;
|
|
534
|
+
};
|
|
535
|
+
supplied: {
|
|
536
|
+
type: string;
|
|
537
|
+
required: string[];
|
|
538
|
+
properties: {
|
|
539
|
+
capabilities: {
|
|
540
|
+
description: string;
|
|
541
|
+
oneOf: ({
|
|
542
|
+
type: string;
|
|
543
|
+
minItems?: never;
|
|
544
|
+
items?: never;
|
|
545
|
+
uniqueItems?: never;
|
|
546
|
+
} | {
|
|
547
|
+
type: string;
|
|
548
|
+
minItems: number;
|
|
549
|
+
items: {
|
|
550
|
+
$ref: string;
|
|
551
|
+
};
|
|
552
|
+
uniqueItems: boolean;
|
|
553
|
+
})[];
|
|
554
|
+
};
|
|
555
|
+
identityMode: {
|
|
556
|
+
oneOf: ({
|
|
557
|
+
type: string;
|
|
558
|
+
$ref?: never;
|
|
559
|
+
} | {
|
|
560
|
+
$ref: string;
|
|
561
|
+
type?: never;
|
|
562
|
+
})[];
|
|
563
|
+
};
|
|
564
|
+
sessionMintLanguage: {
|
|
565
|
+
description: string;
|
|
566
|
+
oneOf: ({
|
|
567
|
+
type: string;
|
|
568
|
+
$ref?: never;
|
|
569
|
+
} | {
|
|
570
|
+
$ref: string;
|
|
571
|
+
type?: never;
|
|
572
|
+
})[];
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
};
|
|
576
|
+
missingFacts: {
|
|
577
|
+
type: string;
|
|
578
|
+
items: {
|
|
579
|
+
$ref: string;
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
steps: {
|
|
583
|
+
type: string;
|
|
584
|
+
items: {
|
|
585
|
+
$ref: string;
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
blocked: {
|
|
589
|
+
description: string;
|
|
590
|
+
type: string;
|
|
591
|
+
items: {
|
|
592
|
+
$ref: string;
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
principles: {
|
|
596
|
+
description: string;
|
|
597
|
+
type: string;
|
|
598
|
+
items: {
|
|
599
|
+
$ref: string;
|
|
600
|
+
};
|
|
601
|
+
};
|
|
602
|
+
warnings: {
|
|
603
|
+
type: string;
|
|
604
|
+
items: {
|
|
605
|
+
$ref: string;
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
decisionSpace: {
|
|
609
|
+
description: string;
|
|
610
|
+
oneOf: ({
|
|
611
|
+
type: string;
|
|
612
|
+
$ref?: never;
|
|
613
|
+
} | {
|
|
614
|
+
$ref: string;
|
|
615
|
+
type?: never;
|
|
616
|
+
})[];
|
|
617
|
+
};
|
|
618
|
+
};
|
|
619
|
+
allOf: ({
|
|
620
|
+
description: string;
|
|
621
|
+
if: {
|
|
622
|
+
properties: {
|
|
623
|
+
status: {
|
|
624
|
+
const: string;
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
required: string[];
|
|
628
|
+
};
|
|
629
|
+
then: {
|
|
630
|
+
properties: {
|
|
631
|
+
missingFacts: {
|
|
632
|
+
minItems: number;
|
|
633
|
+
maxItems?: never;
|
|
634
|
+
};
|
|
635
|
+
steps: {
|
|
636
|
+
maxItems: number;
|
|
637
|
+
minItems?: never;
|
|
638
|
+
};
|
|
639
|
+
blocked?: never;
|
|
640
|
+
};
|
|
641
|
+
required?: never;
|
|
642
|
+
$comment?: never;
|
|
643
|
+
};
|
|
644
|
+
} | {
|
|
645
|
+
description: string;
|
|
646
|
+
if: {
|
|
647
|
+
properties: {
|
|
648
|
+
status: {
|
|
649
|
+
const: string;
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
required: string[];
|
|
653
|
+
};
|
|
654
|
+
then: {
|
|
655
|
+
properties: {
|
|
656
|
+
missingFacts: {
|
|
657
|
+
maxItems: number;
|
|
658
|
+
minItems?: never;
|
|
659
|
+
};
|
|
660
|
+
steps: {
|
|
661
|
+
minItems: number;
|
|
662
|
+
maxItems?: never;
|
|
663
|
+
};
|
|
664
|
+
blocked?: never;
|
|
665
|
+
};
|
|
666
|
+
required: string[];
|
|
667
|
+
$comment: string;
|
|
668
|
+
};
|
|
669
|
+
} | {
|
|
670
|
+
description: string;
|
|
671
|
+
if: {
|
|
672
|
+
properties: {
|
|
673
|
+
status: {
|
|
674
|
+
const: string;
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
required: string[];
|
|
678
|
+
};
|
|
679
|
+
then: {
|
|
680
|
+
properties: {
|
|
681
|
+
missingFacts: {
|
|
682
|
+
maxItems: number;
|
|
683
|
+
minItems?: never;
|
|
684
|
+
};
|
|
685
|
+
steps: {
|
|
686
|
+
maxItems: number;
|
|
687
|
+
minItems?: never;
|
|
688
|
+
};
|
|
689
|
+
blocked: {
|
|
690
|
+
minItems: number;
|
|
691
|
+
};
|
|
692
|
+
};
|
|
693
|
+
required?: never;
|
|
694
|
+
$comment?: never;
|
|
695
|
+
};
|
|
696
|
+
})[];
|
|
697
|
+
$defs: {
|
|
698
|
+
OptionId: {
|
|
699
|
+
description: string;
|
|
700
|
+
type: string;
|
|
701
|
+
pattern: string;
|
|
702
|
+
};
|
|
703
|
+
Capability: {
|
|
704
|
+
description: string;
|
|
705
|
+
$ref: string;
|
|
706
|
+
};
|
|
707
|
+
IdentityMode: {
|
|
708
|
+
description: string;
|
|
709
|
+
$ref: string;
|
|
710
|
+
};
|
|
711
|
+
ProjectType: {
|
|
712
|
+
enum: string[];
|
|
713
|
+
};
|
|
714
|
+
PackageManager: {
|
|
715
|
+
enum: string[];
|
|
716
|
+
};
|
|
717
|
+
Detection: {
|
|
718
|
+
type: string;
|
|
719
|
+
required: string[];
|
|
720
|
+
properties: {
|
|
721
|
+
root: {
|
|
722
|
+
type: string;
|
|
723
|
+
minLength: number;
|
|
724
|
+
};
|
|
725
|
+
projectType: {
|
|
726
|
+
$ref: string;
|
|
727
|
+
};
|
|
728
|
+
projectTypeEvidence: {
|
|
729
|
+
type: string;
|
|
730
|
+
items: {
|
|
731
|
+
type: string;
|
|
732
|
+
};
|
|
733
|
+
};
|
|
734
|
+
packageManager: {
|
|
735
|
+
$ref: string;
|
|
736
|
+
};
|
|
737
|
+
packageManagerEvidence: {
|
|
738
|
+
type: string;
|
|
739
|
+
items: {
|
|
740
|
+
type: string;
|
|
741
|
+
};
|
|
742
|
+
};
|
|
743
|
+
dependencies: {
|
|
744
|
+
type: string;
|
|
745
|
+
items: {
|
|
746
|
+
$ref: string;
|
|
747
|
+
};
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
};
|
|
751
|
+
Dependency: {
|
|
752
|
+
type: string;
|
|
753
|
+
required: string[];
|
|
754
|
+
properties: {
|
|
755
|
+
name: {
|
|
756
|
+
type: string;
|
|
757
|
+
minLength: number;
|
|
758
|
+
};
|
|
759
|
+
declared: {
|
|
760
|
+
oneOf: {
|
|
761
|
+
type: string;
|
|
762
|
+
}[];
|
|
763
|
+
};
|
|
764
|
+
installed: {
|
|
765
|
+
oneOf: {
|
|
766
|
+
type: string;
|
|
767
|
+
}[];
|
|
768
|
+
};
|
|
769
|
+
source: {
|
|
770
|
+
enum: string[];
|
|
771
|
+
};
|
|
772
|
+
scope: {
|
|
773
|
+
enum: string[];
|
|
774
|
+
};
|
|
775
|
+
};
|
|
776
|
+
};
|
|
777
|
+
BlockedCapability: {
|
|
778
|
+
type: string;
|
|
779
|
+
required: string[];
|
|
780
|
+
properties: {
|
|
781
|
+
capability: {
|
|
782
|
+
$ref: string;
|
|
783
|
+
};
|
|
784
|
+
reason: {
|
|
785
|
+
type: string;
|
|
786
|
+
minLength: number;
|
|
787
|
+
};
|
|
788
|
+
unblockOwner: {
|
|
789
|
+
type: string;
|
|
790
|
+
minLength: number;
|
|
791
|
+
};
|
|
792
|
+
requestPath: {
|
|
793
|
+
description: string;
|
|
794
|
+
type: string;
|
|
795
|
+
pattern: string;
|
|
796
|
+
};
|
|
797
|
+
capabilityKeys: {
|
|
798
|
+
description: string;
|
|
799
|
+
type: string;
|
|
800
|
+
items: {
|
|
801
|
+
type: string;
|
|
802
|
+
minLength: number;
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
};
|
|
806
|
+
};
|
|
807
|
+
PlanPrinciple: {
|
|
808
|
+
type: string;
|
|
809
|
+
required: string[];
|
|
810
|
+
properties: {
|
|
811
|
+
treeId: {
|
|
812
|
+
$ref: string;
|
|
813
|
+
};
|
|
814
|
+
id: {
|
|
815
|
+
$ref: string;
|
|
816
|
+
};
|
|
817
|
+
title: {
|
|
818
|
+
type: string;
|
|
819
|
+
minLength: number;
|
|
820
|
+
};
|
|
821
|
+
statement: {
|
|
822
|
+
type: string;
|
|
823
|
+
minLength: number;
|
|
824
|
+
};
|
|
825
|
+
docPath: {
|
|
826
|
+
oneOf: ({
|
|
827
|
+
type: string;
|
|
828
|
+
pattern?: never;
|
|
829
|
+
} | {
|
|
830
|
+
type: string;
|
|
831
|
+
pattern: string;
|
|
832
|
+
})[];
|
|
833
|
+
};
|
|
834
|
+
};
|
|
835
|
+
};
|
|
836
|
+
DecisionSpaceProvenance: {
|
|
837
|
+
type: string;
|
|
838
|
+
required: string[];
|
|
839
|
+
properties: {
|
|
840
|
+
source: {
|
|
841
|
+
type: string;
|
|
842
|
+
minLength: number;
|
|
843
|
+
};
|
|
844
|
+
digest: {
|
|
845
|
+
type: string;
|
|
846
|
+
pattern: string;
|
|
847
|
+
};
|
|
848
|
+
trees: {
|
|
849
|
+
type: string;
|
|
850
|
+
items: {
|
|
851
|
+
$ref: string;
|
|
852
|
+
};
|
|
853
|
+
};
|
|
854
|
+
};
|
|
855
|
+
};
|
|
856
|
+
MissingFact: {
|
|
857
|
+
type: string;
|
|
858
|
+
required: string[];
|
|
859
|
+
properties: {
|
|
860
|
+
fact: {
|
|
861
|
+
enum: string[];
|
|
862
|
+
};
|
|
863
|
+
reason: {
|
|
864
|
+
type: string;
|
|
865
|
+
minLength: number;
|
|
866
|
+
};
|
|
867
|
+
supplyWith: {
|
|
868
|
+
type: string;
|
|
869
|
+
minLength: number;
|
|
870
|
+
};
|
|
871
|
+
};
|
|
872
|
+
};
|
|
873
|
+
PlanStep: {
|
|
874
|
+
type: string;
|
|
875
|
+
required: string[];
|
|
876
|
+
properties: {
|
|
877
|
+
id: {
|
|
878
|
+
type: string;
|
|
879
|
+
pattern: string;
|
|
880
|
+
};
|
|
881
|
+
phase: {
|
|
882
|
+
enum: string[];
|
|
883
|
+
};
|
|
884
|
+
title: {
|
|
885
|
+
type: string;
|
|
886
|
+
minLength: number;
|
|
887
|
+
};
|
|
888
|
+
detail: {
|
|
889
|
+
type: string;
|
|
890
|
+
minLength: number;
|
|
891
|
+
};
|
|
892
|
+
commands: {
|
|
893
|
+
type: string;
|
|
894
|
+
items: {
|
|
895
|
+
type: string;
|
|
896
|
+
minLength: number;
|
|
897
|
+
};
|
|
898
|
+
};
|
|
899
|
+
files: {
|
|
900
|
+
type: string;
|
|
901
|
+
items: {
|
|
902
|
+
type: string;
|
|
903
|
+
minLength: number;
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
};
|
|
907
|
+
};
|
|
908
|
+
PlanWarning: {
|
|
909
|
+
type: string;
|
|
910
|
+
required: string[];
|
|
911
|
+
properties: {
|
|
912
|
+
code: {
|
|
913
|
+
type: string;
|
|
914
|
+
pattern: string;
|
|
915
|
+
};
|
|
916
|
+
message: {
|
|
917
|
+
type: string;
|
|
918
|
+
minLength: number;
|
|
919
|
+
};
|
|
920
|
+
};
|
|
921
|
+
};
|
|
922
|
+
};
|
|
923
|
+
};
|
|
924
|
+
contractPublic: {
|
|
925
|
+
$schema: string;
|
|
926
|
+
$id: string;
|
|
927
|
+
title: string;
|
|
928
|
+
description: string;
|
|
929
|
+
type: string;
|
|
930
|
+
required: string[];
|
|
931
|
+
properties: {
|
|
932
|
+
schemaVersion: {
|
|
933
|
+
const: number;
|
|
934
|
+
};
|
|
935
|
+
kind: {
|
|
936
|
+
const: string;
|
|
937
|
+
};
|
|
938
|
+
generatedAt: {
|
|
939
|
+
type: string;
|
|
940
|
+
format: string;
|
|
941
|
+
description: string;
|
|
942
|
+
};
|
|
943
|
+
digest: {
|
|
944
|
+
type: string;
|
|
945
|
+
required: string[];
|
|
946
|
+
properties: {
|
|
947
|
+
algorithm: {
|
|
948
|
+
const: string;
|
|
949
|
+
description: string;
|
|
950
|
+
};
|
|
951
|
+
value: {
|
|
952
|
+
type: string;
|
|
953
|
+
pattern: string;
|
|
954
|
+
description: string;
|
|
955
|
+
};
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
projection: {
|
|
959
|
+
type: string;
|
|
960
|
+
description: string;
|
|
961
|
+
};
|
|
962
|
+
};
|
|
963
|
+
};
|
|
964
|
+
diagnosis: {
|
|
965
|
+
$schema: string;
|
|
966
|
+
$id: string;
|
|
967
|
+
title: string;
|
|
968
|
+
description: string;
|
|
969
|
+
type: string;
|
|
970
|
+
required: string[];
|
|
971
|
+
properties: {
|
|
972
|
+
schemaVersion: {
|
|
973
|
+
const: number;
|
|
974
|
+
};
|
|
975
|
+
kind: {
|
|
976
|
+
const: string;
|
|
977
|
+
};
|
|
978
|
+
generatedBy: {
|
|
979
|
+
$ref: string;
|
|
980
|
+
};
|
|
981
|
+
endpoint: {
|
|
982
|
+
type: string;
|
|
983
|
+
required: string[];
|
|
984
|
+
properties: {
|
|
985
|
+
baseUrl: {
|
|
986
|
+
type: string;
|
|
987
|
+
minLength: number;
|
|
988
|
+
};
|
|
989
|
+
path: {
|
|
990
|
+
type: string;
|
|
991
|
+
minLength: number;
|
|
992
|
+
};
|
|
993
|
+
};
|
|
994
|
+
};
|
|
995
|
+
requestId: {
|
|
996
|
+
description: string;
|
|
997
|
+
oneOf: ({
|
|
998
|
+
type: string;
|
|
999
|
+
minLength?: never;
|
|
1000
|
+
} | {
|
|
1001
|
+
type: string;
|
|
1002
|
+
minLength: number;
|
|
1003
|
+
})[];
|
|
1004
|
+
};
|
|
1005
|
+
evaluatedAt: {
|
|
1006
|
+
type: string;
|
|
1007
|
+
};
|
|
1008
|
+
client: {
|
|
1009
|
+
type: string;
|
|
1010
|
+
required: string[];
|
|
1011
|
+
properties: {
|
|
1012
|
+
id: {
|
|
1013
|
+
type: string;
|
|
1014
|
+
};
|
|
1015
|
+
slug: {
|
|
1016
|
+
type: string;
|
|
1017
|
+
};
|
|
1018
|
+
};
|
|
1019
|
+
};
|
|
1020
|
+
auth: {
|
|
1021
|
+
type: string;
|
|
1022
|
+
required: string[];
|
|
1023
|
+
properties: {
|
|
1024
|
+
mode: {
|
|
1025
|
+
type: string;
|
|
1026
|
+
};
|
|
1027
|
+
anonymous: {
|
|
1028
|
+
type: string;
|
|
1029
|
+
};
|
|
1030
|
+
userRefPrefix: {
|
|
1031
|
+
description: string;
|
|
1032
|
+
type: string;
|
|
1033
|
+
maxLength: number;
|
|
1034
|
+
};
|
|
1035
|
+
userRole: {
|
|
1036
|
+
type: string;
|
|
1037
|
+
};
|
|
1038
|
+
roleSource: {
|
|
1039
|
+
type: string;
|
|
1040
|
+
};
|
|
1041
|
+
sessionId: {
|
|
1042
|
+
type: string;
|
|
1043
|
+
};
|
|
1044
|
+
partnerKeyScopeResolved: {
|
|
1045
|
+
type: string;
|
|
1046
|
+
};
|
|
1047
|
+
};
|
|
1048
|
+
};
|
|
1049
|
+
scope: {
|
|
1050
|
+
type: string;
|
|
1051
|
+
required: string[];
|
|
1052
|
+
properties: {
|
|
1053
|
+
provisioned: {
|
|
1054
|
+
type: string;
|
|
1055
|
+
};
|
|
1056
|
+
resolverStatus: {
|
|
1057
|
+
type: string;
|
|
1058
|
+
};
|
|
1059
|
+
revision: {
|
|
1060
|
+
type: string;
|
|
1061
|
+
minimum: number;
|
|
1062
|
+
};
|
|
1063
|
+
region: {
|
|
1064
|
+
type: string;
|
|
1065
|
+
};
|
|
1066
|
+
levels: {
|
|
1067
|
+
type: string;
|
|
1068
|
+
items: {
|
|
1069
|
+
type: string;
|
|
1070
|
+
required: string[];
|
|
1071
|
+
properties: {
|
|
1072
|
+
level: {
|
|
1073
|
+
type: string;
|
|
1074
|
+
minimum: number;
|
|
1075
|
+
maximum: number;
|
|
1076
|
+
};
|
|
1077
|
+
kind: {
|
|
1078
|
+
type: string;
|
|
1079
|
+
};
|
|
1080
|
+
present: {
|
|
1081
|
+
type: string;
|
|
1082
|
+
};
|
|
1083
|
+
};
|
|
1084
|
+
};
|
|
1085
|
+
};
|
|
1086
|
+
};
|
|
1087
|
+
};
|
|
1088
|
+
summary: {
|
|
1089
|
+
type: string;
|
|
1090
|
+
required: string[];
|
|
1091
|
+
properties: {
|
|
1092
|
+
total: {
|
|
1093
|
+
type: string;
|
|
1094
|
+
minimum: number;
|
|
1095
|
+
};
|
|
1096
|
+
available: {
|
|
1097
|
+
type: string;
|
|
1098
|
+
minimum: number;
|
|
1099
|
+
};
|
|
1100
|
+
unavailable: {
|
|
1101
|
+
type: string;
|
|
1102
|
+
minimum: number;
|
|
1103
|
+
};
|
|
1104
|
+
byReasonCode: {
|
|
1105
|
+
description: string;
|
|
1106
|
+
type: string;
|
|
1107
|
+
additionalProperties: {
|
|
1108
|
+
type: string;
|
|
1109
|
+
minimum: number;
|
|
1110
|
+
};
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
};
|
|
1114
|
+
capabilities: {
|
|
1115
|
+
type: string;
|
|
1116
|
+
items: {
|
|
1117
|
+
$ref: string;
|
|
1118
|
+
};
|
|
1119
|
+
};
|
|
1120
|
+
};
|
|
1121
|
+
$defs: {
|
|
1122
|
+
GeneratedBy: {
|
|
1123
|
+
type: string;
|
|
1124
|
+
required: string[];
|
|
1125
|
+
properties: {
|
|
1126
|
+
name: {
|
|
1127
|
+
type: string;
|
|
1128
|
+
minLength: number;
|
|
1129
|
+
};
|
|
1130
|
+
version: {
|
|
1131
|
+
type: string;
|
|
1132
|
+
minLength: number;
|
|
1133
|
+
};
|
|
1134
|
+
};
|
|
1135
|
+
};
|
|
1136
|
+
CapabilityDiagnosis: {
|
|
1137
|
+
type: string;
|
|
1138
|
+
required: string[];
|
|
1139
|
+
properties: {
|
|
1140
|
+
key: {
|
|
1141
|
+
type: string;
|
|
1142
|
+
minLength: number;
|
|
1143
|
+
};
|
|
1144
|
+
available: {
|
|
1145
|
+
type: string;
|
|
1146
|
+
};
|
|
1147
|
+
reasonCode: {
|
|
1148
|
+
description: string;
|
|
1149
|
+
type: string;
|
|
1150
|
+
minLength: number;
|
|
1151
|
+
};
|
|
1152
|
+
reason: {
|
|
1153
|
+
description: string;
|
|
1154
|
+
type: string;
|
|
1155
|
+
};
|
|
1156
|
+
entitlementReasonCode: {
|
|
1157
|
+
description: string;
|
|
1158
|
+
type: string;
|
|
1159
|
+
minLength: number;
|
|
1160
|
+
};
|
|
1161
|
+
decidedAtLevel: {
|
|
1162
|
+
oneOf: ({
|
|
1163
|
+
type: string;
|
|
1164
|
+
minimum?: never;
|
|
1165
|
+
maximum?: never;
|
|
1166
|
+
} | {
|
|
1167
|
+
type: string;
|
|
1168
|
+
minimum: number;
|
|
1169
|
+
maximum: number;
|
|
1170
|
+
})[];
|
|
1171
|
+
};
|
|
1172
|
+
decidedAtKind: {
|
|
1173
|
+
oneOf: {
|
|
1174
|
+
type: string;
|
|
1175
|
+
}[];
|
|
1176
|
+
};
|
|
1177
|
+
surface: {
|
|
1178
|
+
description: string;
|
|
1179
|
+
oneOf: {
|
|
1180
|
+
type: string;
|
|
1181
|
+
}[];
|
|
1182
|
+
};
|
|
1183
|
+
nextAction: {
|
|
1184
|
+
description: string;
|
|
1185
|
+
type: string;
|
|
1186
|
+
minLength: number;
|
|
1187
|
+
};
|
|
1188
|
+
};
|
|
1189
|
+
};
|
|
1190
|
+
};
|
|
1191
|
+
};
|
|
1192
|
+
sandboxCreate: {
|
|
1193
|
+
$schema: string;
|
|
1194
|
+
$id: string;
|
|
1195
|
+
title: string;
|
|
1196
|
+
description: string;
|
|
1197
|
+
type: string;
|
|
1198
|
+
required: string[];
|
|
1199
|
+
properties: {
|
|
1200
|
+
schemaVersion: {
|
|
1201
|
+
const: number;
|
|
1202
|
+
};
|
|
1203
|
+
kind: {
|
|
1204
|
+
const: string;
|
|
1205
|
+
};
|
|
1206
|
+
generatedBy: {
|
|
1207
|
+
type: string;
|
|
1208
|
+
required: string[];
|
|
1209
|
+
properties: {
|
|
1210
|
+
name: {
|
|
1211
|
+
type: string;
|
|
1212
|
+
minLength: number;
|
|
1213
|
+
};
|
|
1214
|
+
version: {
|
|
1215
|
+
type: string;
|
|
1216
|
+
minLength: number;
|
|
1217
|
+
};
|
|
1218
|
+
};
|
|
1219
|
+
};
|
|
1220
|
+
status: {
|
|
1221
|
+
enum: string[];
|
|
1222
|
+
};
|
|
1223
|
+
requestId: {
|
|
1224
|
+
oneOf: ({
|
|
1225
|
+
type: string;
|
|
1226
|
+
minLength?: never;
|
|
1227
|
+
} | {
|
|
1228
|
+
type: string;
|
|
1229
|
+
minLength: number;
|
|
1230
|
+
})[];
|
|
1231
|
+
};
|
|
1232
|
+
clientId: {
|
|
1233
|
+
type: string;
|
|
1234
|
+
};
|
|
1235
|
+
keyId: {
|
|
1236
|
+
type: string;
|
|
1237
|
+
minLength: number;
|
|
1238
|
+
};
|
|
1239
|
+
label: {
|
|
1240
|
+
oneOf: {
|
|
1241
|
+
type: string;
|
|
1242
|
+
}[];
|
|
1243
|
+
};
|
|
1244
|
+
createdAt: {
|
|
1245
|
+
oneOf: {
|
|
1246
|
+
type: string;
|
|
1247
|
+
}[];
|
|
1248
|
+
};
|
|
1249
|
+
claimUrl: {
|
|
1250
|
+
description: string;
|
|
1251
|
+
oneOf: ({
|
|
1252
|
+
type: string;
|
|
1253
|
+
minLength?: never;
|
|
1254
|
+
} | {
|
|
1255
|
+
type: string;
|
|
1256
|
+
minLength: number;
|
|
1257
|
+
})[];
|
|
1258
|
+
};
|
|
1259
|
+
claimExpiresAt: {
|
|
1260
|
+
oneOf: {
|
|
1261
|
+
type: string;
|
|
1262
|
+
}[];
|
|
1263
|
+
};
|
|
1264
|
+
secretDisclosed: {
|
|
1265
|
+
description: string;
|
|
1266
|
+
const: boolean;
|
|
1267
|
+
};
|
|
1268
|
+
secretDiscarded: {
|
|
1269
|
+
description: string;
|
|
1270
|
+
type: string;
|
|
1271
|
+
};
|
|
1272
|
+
orphanedKey: {
|
|
1273
|
+
description: string;
|
|
1274
|
+
enum: string[];
|
|
1275
|
+
};
|
|
1276
|
+
nextActions: {
|
|
1277
|
+
type: string;
|
|
1278
|
+
minItems: number;
|
|
1279
|
+
items: {
|
|
1280
|
+
type: string;
|
|
1281
|
+
minLength: number;
|
|
1282
|
+
};
|
|
1283
|
+
};
|
|
1284
|
+
};
|
|
1285
|
+
allOf: ({
|
|
1286
|
+
if: {
|
|
1287
|
+
properties: {
|
|
1288
|
+
status: {
|
|
1289
|
+
const: string;
|
|
1290
|
+
};
|
|
1291
|
+
};
|
|
1292
|
+
required: string[];
|
|
1293
|
+
};
|
|
1294
|
+
then: {
|
|
1295
|
+
properties: {
|
|
1296
|
+
claimUrl: {
|
|
1297
|
+
type: string;
|
|
1298
|
+
minLength?: never;
|
|
1299
|
+
};
|
|
1300
|
+
};
|
|
1301
|
+
};
|
|
1302
|
+
} | {
|
|
1303
|
+
if: {
|
|
1304
|
+
properties: {
|
|
1305
|
+
status: {
|
|
1306
|
+
const: string;
|
|
1307
|
+
};
|
|
1308
|
+
};
|
|
1309
|
+
required: string[];
|
|
1310
|
+
};
|
|
1311
|
+
then: {
|
|
1312
|
+
properties: {
|
|
1313
|
+
claimUrl: {
|
|
1314
|
+
type: string;
|
|
1315
|
+
minLength: number;
|
|
1316
|
+
};
|
|
1317
|
+
};
|
|
1318
|
+
};
|
|
1319
|
+
})[];
|
|
1320
|
+
};
|
|
1321
|
+
verification: {
|
|
1322
|
+
$schema: string;
|
|
1323
|
+
$id: string;
|
|
1324
|
+
title: string;
|
|
1325
|
+
description: string;
|
|
1326
|
+
type: string;
|
|
1327
|
+
required: string[];
|
|
1328
|
+
properties: {
|
|
1329
|
+
schemaVersion: {
|
|
1330
|
+
const: number;
|
|
1331
|
+
};
|
|
1332
|
+
kind: {
|
|
1333
|
+
const: string;
|
|
1334
|
+
};
|
|
1335
|
+
generatedBy: {
|
|
1336
|
+
type: string;
|
|
1337
|
+
required: string[];
|
|
1338
|
+
properties: {
|
|
1339
|
+
name: {
|
|
1340
|
+
type: string;
|
|
1341
|
+
minLength: number;
|
|
1342
|
+
};
|
|
1343
|
+
version: {
|
|
1344
|
+
type: string;
|
|
1345
|
+
minLength: number;
|
|
1346
|
+
};
|
|
1347
|
+
};
|
|
1348
|
+
};
|
|
1349
|
+
status: {
|
|
1350
|
+
description: string;
|
|
1351
|
+
enum: string[];
|
|
1352
|
+
};
|
|
1353
|
+
baseUrl: {
|
|
1354
|
+
type: string;
|
|
1355
|
+
minLength: number;
|
|
1356
|
+
};
|
|
1357
|
+
summary: {
|
|
1358
|
+
type: string;
|
|
1359
|
+
required: string[];
|
|
1360
|
+
properties: {
|
|
1361
|
+
passed: {
|
|
1362
|
+
type: string;
|
|
1363
|
+
minimum: number;
|
|
1364
|
+
};
|
|
1365
|
+
failed: {
|
|
1366
|
+
type: string;
|
|
1367
|
+
minimum: number;
|
|
1368
|
+
};
|
|
1369
|
+
skipped: {
|
|
1370
|
+
type: string;
|
|
1371
|
+
minimum: number;
|
|
1372
|
+
};
|
|
1373
|
+
};
|
|
1374
|
+
};
|
|
1375
|
+
steps: {
|
|
1376
|
+
type: string;
|
|
1377
|
+
minItems: number;
|
|
1378
|
+
items: {
|
|
1379
|
+
type: string;
|
|
1380
|
+
required: string[];
|
|
1381
|
+
properties: {
|
|
1382
|
+
id: {
|
|
1383
|
+
type: string;
|
|
1384
|
+
minLength: number;
|
|
1385
|
+
};
|
|
1386
|
+
title: {
|
|
1387
|
+
type: string;
|
|
1388
|
+
minLength: number;
|
|
1389
|
+
};
|
|
1390
|
+
result: {
|
|
1391
|
+
enum: string[];
|
|
1392
|
+
};
|
|
1393
|
+
reason: {
|
|
1394
|
+
description: string;
|
|
1395
|
+
type: string;
|
|
1396
|
+
minLength: number;
|
|
1397
|
+
};
|
|
1398
|
+
requestId: {
|
|
1399
|
+
oneOf: ({
|
|
1400
|
+
type: string;
|
|
1401
|
+
minLength?: never;
|
|
1402
|
+
} | {
|
|
1403
|
+
type: string;
|
|
1404
|
+
minLength: number;
|
|
1405
|
+
})[];
|
|
1406
|
+
};
|
|
1407
|
+
details: {
|
|
1408
|
+
type: string;
|
|
1409
|
+
items: {
|
|
1410
|
+
type: string;
|
|
1411
|
+
};
|
|
1412
|
+
};
|
|
1413
|
+
};
|
|
1414
|
+
};
|
|
1415
|
+
};
|
|
1416
|
+
};
|
|
1417
|
+
};
|
|
1418
|
+
errorExplanation: {
|
|
1419
|
+
$schema: string;
|
|
1420
|
+
$id: string;
|
|
1421
|
+
title: string;
|
|
1422
|
+
description: string;
|
|
1423
|
+
type: string;
|
|
1424
|
+
required: string[];
|
|
1425
|
+
properties: {
|
|
1426
|
+
schemaVersion: {
|
|
1427
|
+
const: number;
|
|
1428
|
+
};
|
|
1429
|
+
kind: {
|
|
1430
|
+
const: string;
|
|
1431
|
+
};
|
|
1432
|
+
generatedBy: {
|
|
1433
|
+
type: string;
|
|
1434
|
+
required: string[];
|
|
1435
|
+
properties: {
|
|
1436
|
+
name: {
|
|
1437
|
+
type: string;
|
|
1438
|
+
minLength: number;
|
|
1439
|
+
};
|
|
1440
|
+
version: {
|
|
1441
|
+
type: string;
|
|
1442
|
+
minLength: number;
|
|
1443
|
+
};
|
|
1444
|
+
};
|
|
1445
|
+
};
|
|
1446
|
+
code: {
|
|
1447
|
+
description: string;
|
|
1448
|
+
type: string;
|
|
1449
|
+
minLength: number;
|
|
1450
|
+
};
|
|
1451
|
+
source: {
|
|
1452
|
+
type: string;
|
|
1453
|
+
required: string[];
|
|
1454
|
+
properties: {
|
|
1455
|
+
path: {
|
|
1456
|
+
type: string;
|
|
1457
|
+
minLength: number;
|
|
1458
|
+
};
|
|
1459
|
+
digest: {
|
|
1460
|
+
type: string;
|
|
1461
|
+
pattern: string;
|
|
1462
|
+
};
|
|
1463
|
+
};
|
|
1464
|
+
};
|
|
1465
|
+
envelope: {
|
|
1466
|
+
type: string;
|
|
1467
|
+
minLength: number;
|
|
1468
|
+
};
|
|
1469
|
+
httpStatus: {
|
|
1470
|
+
type: string;
|
|
1471
|
+
minimum: number;
|
|
1472
|
+
};
|
|
1473
|
+
namingFamily: {
|
|
1474
|
+
type: string;
|
|
1475
|
+
minLength: number;
|
|
1476
|
+
};
|
|
1477
|
+
retryable: {
|
|
1478
|
+
type: string;
|
|
1479
|
+
};
|
|
1480
|
+
confirmedOnWire: {
|
|
1481
|
+
description: string;
|
|
1482
|
+
type: string;
|
|
1483
|
+
};
|
|
1484
|
+
surfaces: {
|
|
1485
|
+
type: string;
|
|
1486
|
+
items: {
|
|
1487
|
+
type: string;
|
|
1488
|
+
};
|
|
1489
|
+
};
|
|
1490
|
+
cause: {
|
|
1491
|
+
type: string;
|
|
1492
|
+
};
|
|
1493
|
+
display: {
|
|
1494
|
+
description: string;
|
|
1495
|
+
type: string;
|
|
1496
|
+
};
|
|
1497
|
+
docUrl: {
|
|
1498
|
+
type: string;
|
|
1499
|
+
};
|
|
1500
|
+
nextAction: {
|
|
1501
|
+
type: string;
|
|
1502
|
+
minLength: number;
|
|
1503
|
+
};
|
|
1504
|
+
};
|
|
1505
|
+
};
|
|
1506
|
+
}>;
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* THE DIGEST RULE, stated once so both sides of the wire can implement it independently:
|
|
1510
|
+
*
|
|
1511
|
+
* digest.value = lowercase hex sha256( utf8( canonicalJson( envelope.projection ) ) )
|
|
1512
|
+
*
|
|
1513
|
+
* Three consequences worth being explicit about, because each one is a bug someone would
|
|
1514
|
+
* otherwise hit:
|
|
1515
|
+
*
|
|
1516
|
+
* 1. The digest covers `projection` ONLY. `generatedAt`, `kind`, `schemaVersion`, and the
|
|
1517
|
+
* digest field itself are outside it, so re-stamping a projection with a new timestamp does
|
|
1518
|
+
* not invalidate a digest, and the producer never has to solve the self-reference problem
|
|
1519
|
+
* of hashing a document that contains its own hash.
|
|
1520
|
+
* 2. Canonicalisation sorts object keys, so a producer that serialises in insertion order and
|
|
1521
|
+
* a producer that serialises alphabetically agree.
|
|
1522
|
+
* 3. The comparison is constant time. A digest is not a secret, so this is belt and braces
|
|
1523
|
+
* rather than a requirement, but the cost is a few microseconds and the habit is worth more.
|
|
1524
|
+
*/
|
|
1525
|
+
interface DigestCheck {
|
|
1526
|
+
readonly matches: boolean;
|
|
1527
|
+
readonly expected: string;
|
|
1528
|
+
readonly actual: string;
|
|
1529
|
+
}
|
|
1530
|
+
/** Compute the digest of a projection value under the rule above. */
|
|
1531
|
+
declare function digestOfProjection(projection: JsonValue): string;
|
|
1532
|
+
/** Length-independent, value-independent comparison of two hex digests. */
|
|
1533
|
+
declare function digestsEqual(left: string, right: string): boolean;
|
|
1534
|
+
/** Recompute and compare in one step. */
|
|
1535
|
+
declare function verifyDigest(projection: JsonValue, declared: string): DigestCheck;
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* Load a contract-public projection from disk and answer three questions in order, stopping at
|
|
1539
|
+
* the first failure because the later answers are meaningless without the earlier ones:
|
|
1540
|
+
*
|
|
1541
|
+
* 1. Is it there and is it JSON?
|
|
1542
|
+
* 2. Does the envelope match the committed schema?
|
|
1543
|
+
* 3. Does the declared digest match the projection it wraps?
|
|
1544
|
+
*
|
|
1545
|
+
* Entirely offline and entirely local. The real projection is produced by the contract lane;
|
|
1546
|
+
* this reads whatever file it is pointed at, which is why it works today against a fixture and
|
|
1547
|
+
* will work unchanged against the real artifact.
|
|
1548
|
+
*/
|
|
1549
|
+
type ContractValidationOutcome = {
|
|
1550
|
+
readonly kind: 'file_not_found';
|
|
1551
|
+
readonly path: string;
|
|
1552
|
+
} | {
|
|
1553
|
+
readonly kind: 'invalid_json';
|
|
1554
|
+
readonly path: string;
|
|
1555
|
+
readonly detail: string;
|
|
1556
|
+
} | {
|
|
1557
|
+
readonly kind: 'invalid_contract';
|
|
1558
|
+
readonly path: string;
|
|
1559
|
+
readonly violations: readonly SchemaViolation[];
|
|
1560
|
+
} | {
|
|
1561
|
+
readonly kind: 'digest_mismatch';
|
|
1562
|
+
readonly path: string;
|
|
1563
|
+
readonly digest: DigestCheck;
|
|
1564
|
+
} | {
|
|
1565
|
+
readonly kind: 'valid';
|
|
1566
|
+
readonly path: string;
|
|
1567
|
+
readonly digest: DigestCheck;
|
|
1568
|
+
readonly generatedAt: string;
|
|
1569
|
+
readonly projectionKeys: readonly string[];
|
|
1570
|
+
/**
|
|
1571
|
+
* The parsed projection this validation just proved intact.
|
|
1572
|
+
*
|
|
1573
|
+
* Carried out rather than thrown away because the alternative is what the error registry
|
|
1574
|
+
* used to do: call this function, take the keys, then re-read and re-JSON.parse the SAME
|
|
1575
|
+
* file. Two reads of one path is a race (a projection rewritten between them by a
|
|
1576
|
+
* concurrent `tools.contract.compile --write` surfaces as a raw SyntaxError escaping a
|
|
1577
|
+
* command, in a package whose whole design is that every anticipated failure settles as a
|
|
1578
|
+
* CommandOutcome), and the second parse had no try/catch while the one above does.
|
|
1579
|
+
*/
|
|
1580
|
+
readonly projection: JsonObject;
|
|
1581
|
+
};
|
|
1582
|
+
/** Resolve a possibly relative `--path` against the injected cwd. Never touches process.cwd. */
|
|
1583
|
+
declare function resolveContractPath(cwd: string, path: string): string;
|
|
1584
|
+
declare function validateContractFile(fs: FileSystemPort, path: string): ContractValidationOutcome;
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* THE ERROR REGISTRY, read from the contract-public projection rather than restated here.
|
|
1588
|
+
*
|
|
1589
|
+
* The platform compiles `contract/errors.yaml` into the redacted projection; this module reads
|
|
1590
|
+
* the `errors.yaml` document out of that projection and nothing else. Copying the table into
|
|
1591
|
+
* TypeScript would have been less code today and a guaranteed drift tomorrow: the wire codes
|
|
1592
|
+
* change when a handler changes, and only the compiler sees that happen.
|
|
1593
|
+
*
|
|
1594
|
+
* The registry is reachable ONLY through `validateContractFile`, so the envelope schema and the
|
|
1595
|
+
* digest are both checked before a single code is trusted. An operator running
|
|
1596
|
+
* `everfur explain-error` against a file someone edited gets a digest mismatch, not a confident
|
|
1597
|
+
* answer from a tampered table.
|
|
1598
|
+
*
|
|
1599
|
+
* `internal` and `evidence` are absent from the projection by design (the compiler redacts
|
|
1600
|
+
* them), which is why this shape has no field for either.
|
|
1601
|
+
*/
|
|
1602
|
+
interface ErrorRegistryEntry {
|
|
1603
|
+
/** Exactly the string the wire carries. Not normalised; three naming families are live. */
|
|
1604
|
+
readonly code: string;
|
|
1605
|
+
readonly envelope: string;
|
|
1606
|
+
readonly httpStatus: number;
|
|
1607
|
+
readonly namingFamily: string;
|
|
1608
|
+
readonly retryable: boolean;
|
|
1609
|
+
/** False means the code exists in source but no live route was traced emitting it. */
|
|
1610
|
+
readonly confirmedOnWire: boolean;
|
|
1611
|
+
readonly surfaces: readonly string[];
|
|
1612
|
+
/** Safe copy a client may show a person as-is. */
|
|
1613
|
+
readonly display: string;
|
|
1614
|
+
readonly docUrl: string;
|
|
1615
|
+
/** The mechanical cause, authored by the contract lane. */
|
|
1616
|
+
readonly note: string;
|
|
1617
|
+
}
|
|
1618
|
+
interface ErrorRegistry {
|
|
1619
|
+
lookup(code: string): ErrorRegistryEntry | null;
|
|
1620
|
+
readonly codes: readonly string[];
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* The registry a command falls back to when no projection was supplied. Every lookup misses,
|
|
1624
|
+
* which downgrades an HTTP failure's rendering to code plus status rather than inventing copy.
|
|
1625
|
+
*/
|
|
1626
|
+
declare const EMPTY_REGISTRY: ErrorRegistry;
|
|
1627
|
+
/** The projection document key the compiler writes the registry under. */
|
|
1628
|
+
declare const ERRORS_DOCUMENT_KEY = "errors.yaml";
|
|
1629
|
+
/**
|
|
1630
|
+
* Narrow one raw projection row. A row missing `code` is dropped rather than defaulted: an
|
|
1631
|
+
* entry whose identity is unknown cannot be looked up, so keeping it would only inflate a count.
|
|
1632
|
+
* Every other field falls back to a neutral value, because a registry that refuses to load over
|
|
1633
|
+
* one missing `note` is a registry that stops answering during the incident it exists for.
|
|
1634
|
+
*/
|
|
1635
|
+
declare function entryFromRow(row: unknown): ErrorRegistryEntry | null;
|
|
1636
|
+
type RegistryLoad = {
|
|
1637
|
+
readonly kind: 'loaded';
|
|
1638
|
+
readonly registry: ErrorRegistry;
|
|
1639
|
+
readonly path: string;
|
|
1640
|
+
readonly digest: string;
|
|
1641
|
+
} | {
|
|
1642
|
+
readonly kind: 'no_registry';
|
|
1643
|
+
readonly path: string;
|
|
1644
|
+
} | {
|
|
1645
|
+
readonly kind: 'contract_rejected';
|
|
1646
|
+
readonly outcome: ContractValidationOutcome;
|
|
1647
|
+
};
|
|
1648
|
+
/**
|
|
1649
|
+
* Load the registry out of a contract-public envelope on disk. Contract failures are handed
|
|
1650
|
+
* back unchanged so the caller can reuse `contract validate`'s exact error rendering rather
|
|
1651
|
+
* than inventing a second, subtly different set of messages for the same three failures.
|
|
1652
|
+
*/
|
|
1653
|
+
declare function loadErrorRegistry(fs: FileSystemPort, path: string): RegistryLoad;
|
|
1654
|
+
|
|
1655
|
+
declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
1656
|
+
interface ApiSuccess<T> {
|
|
1657
|
+
readonly ok: true;
|
|
1658
|
+
readonly value: T;
|
|
1659
|
+
readonly status: number;
|
|
1660
|
+
/** The server's id, or null when the response carried none. */
|
|
1661
|
+
readonly requestId: string | null;
|
|
1662
|
+
readonly attempts: number;
|
|
1663
|
+
}
|
|
1664
|
+
type ApiFailure = {
|
|
1665
|
+
readonly ok: false;
|
|
1666
|
+
readonly kind: 'network';
|
|
1667
|
+
readonly method: HttpMethod;
|
|
1668
|
+
readonly path: string;
|
|
1669
|
+
readonly detail: string;
|
|
1670
|
+
readonly attempts: number;
|
|
1671
|
+
readonly requestId: null;
|
|
1672
|
+
} | {
|
|
1673
|
+
readonly ok: false;
|
|
1674
|
+
readonly kind: 'timeout';
|
|
1675
|
+
readonly method: HttpMethod;
|
|
1676
|
+
readonly path: string;
|
|
1677
|
+
readonly timeoutMs: number;
|
|
1678
|
+
readonly attempts: number;
|
|
1679
|
+
readonly requestId: null;
|
|
1680
|
+
} | {
|
|
1681
|
+
readonly ok: false;
|
|
1682
|
+
readonly kind: 'http';
|
|
1683
|
+
readonly method: HttpMethod;
|
|
1684
|
+
readonly path: string;
|
|
1685
|
+
readonly status: number;
|
|
1686
|
+
/** The wire code, or null when the body carried none. */
|
|
1687
|
+
readonly code: string | null;
|
|
1688
|
+
/**
|
|
1689
|
+
* UNTRUSTED server free text, kept for a debugger and NEVER rendered.
|
|
1690
|
+
*
|
|
1691
|
+
* This field used to be called `message` and used to be interpolated into the failure
|
|
1692
|
+
* summary, on the assumption that "the API's handlers emit vetted copy". They do not:
|
|
1693
|
+
* `grep 'detail=str(exc'` across the platform's api service finds twenty-odd handlers
|
|
1694
|
+
* that put a caught exception's `str()` straight into `detail`, so an upstream key, a
|
|
1695
|
+
* connection string, or a bearer becomes CLI output on a path where the operator typed
|
|
1696
|
+
* nothing sensitive. `describeFailure` now renders operator-owned data only.
|
|
1697
|
+
*
|
|
1698
|
+
* It is defined NON-ENUMERABLE (see `httpFailure` below), mirroring the SDK's
|
|
1699
|
+
* `EverfurError.wireMessage`, so the serialisers that normally walk an object,
|
|
1700
|
+
* `JSON.stringify`, spread, `Object.entries`, cannot carry it into a `--json` document
|
|
1701
|
+
* either. Only a deliberate read reaches it.
|
|
1702
|
+
*/
|
|
1703
|
+
readonly wireMessage: string;
|
|
1704
|
+
readonly entry: ErrorRegistryEntry | null;
|
|
1705
|
+
readonly retryable: boolean;
|
|
1706
|
+
readonly requestId: string | null;
|
|
1707
|
+
readonly attempts: number;
|
|
1708
|
+
} | {
|
|
1709
|
+
readonly ok: false;
|
|
1710
|
+
readonly kind: 'malformed';
|
|
1711
|
+
readonly method: HttpMethod;
|
|
1712
|
+
readonly path: string;
|
|
1713
|
+
readonly status: number;
|
|
1714
|
+
readonly detail: string;
|
|
1715
|
+
readonly requestId: string | null;
|
|
1716
|
+
readonly attempts: number;
|
|
1717
|
+
};
|
|
1718
|
+
type ApiResult<T> = ApiSuccess<T> | ApiFailure;
|
|
1719
|
+
interface ApiClientOptions {
|
|
1720
|
+
readonly transport: TransportPort;
|
|
1721
|
+
readonly clock: ClockPort;
|
|
1722
|
+
readonly endpoint: ResolvedEndpoint;
|
|
1723
|
+
readonly registry?: ErrorRegistry;
|
|
1724
|
+
readonly timeoutMs?: number;
|
|
1725
|
+
/** Caller supplied correlation id. Sent only when it is a shape the API will reuse. */
|
|
1726
|
+
readonly requestId?: string | null;
|
|
1727
|
+
/** Attempts allowed for an IDEMPOTENT call. Non-idempotent calls always get exactly one. */
|
|
1728
|
+
readonly maxAttempts?: number;
|
|
1729
|
+
/** Delay between attempts, in milliseconds. */
|
|
1730
|
+
readonly retryDelayMs?: number;
|
|
1731
|
+
}
|
|
1732
|
+
interface RequestSpec {
|
|
1733
|
+
readonly method: HttpMethod;
|
|
1734
|
+
/** API-relative, for example `/widget/v1/diagnostics`. The prefix is added here. */
|
|
1735
|
+
readonly path: string;
|
|
1736
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
1737
|
+
/** Header name to credential. Revealed into the outgoing header map only. */
|
|
1738
|
+
readonly credentials?: Readonly<Record<string, Credential>>;
|
|
1739
|
+
readonly body?: JsonValue;
|
|
1740
|
+
/** Statuses that are a successful outcome for this call, in addition to 2xx. */
|
|
1741
|
+
readonly acceptStatuses?: readonly number[];
|
|
1742
|
+
}
|
|
1743
|
+
interface ApiClient {
|
|
1744
|
+
request(spec: RequestSpec): Promise<ApiResult<JsonValue>>;
|
|
1745
|
+
/** The resolved base URL, for reporting. Never a credential. */
|
|
1746
|
+
readonly baseUrl: string;
|
|
1747
|
+
}
|
|
1748
|
+
declare function clampTimeout(value: number): number;
|
|
1749
|
+
/** True when the value is a shape `RequestIDMiddleware` will reuse rather than replace. */
|
|
1750
|
+
declare function isCorrelatableRequestId(value: string): boolean;
|
|
1751
|
+
/**
|
|
1752
|
+
* Read the wire code out of an error body. Three shapes are live at once and the registry says
|
|
1753
|
+
* so, so this reads all three rather than pretending there is one:
|
|
1754
|
+
*
|
|
1755
|
+
* {"code": "...", "message": ...} the status-mapped envelope
|
|
1756
|
+
* {"error": "...", "message": ...} a dict-detail dependency, flattened
|
|
1757
|
+
* {"detail": {"error": "...", ...}} the same, unflattened by a passthrough handler
|
|
1758
|
+
*
|
|
1759
|
+
* THE CODE IS TRUSTED, THE TEXT IS NOT. `code` is a wire enum this CLI matches against the
|
|
1760
|
+
* compiled registry, so it is operator-safe to print. `wireMessage` is whatever the server
|
|
1761
|
+
* chose to say, from any of the four branches below, and every one of those branches can carry
|
|
1762
|
+
* a caught exception's `str()`. It is named `wireMessage` rather than `message` precisely so
|
|
1763
|
+
* that no future caller reads it as "the copy to show".
|
|
1764
|
+
*/
|
|
1765
|
+
declare function errorCodeOf(body: unknown): {
|
|
1766
|
+
readonly code: string | null;
|
|
1767
|
+
readonly wireMessage: string;
|
|
1768
|
+
};
|
|
1769
|
+
declare function createApiClient(options: ApiClientOptions): ApiClient;
|
|
1770
|
+
/**
|
|
1771
|
+
* One-line human summary of a failure. Assembled from OPERATOR-OWNED DATA ONLY: the method and
|
|
1772
|
+
* path the operator's own command named, the status, the wire code, and, when that code
|
|
1773
|
+
* resolves, the registry's own vetted `display` copy. Never a credential and never server free
|
|
1774
|
+
* text, which are the same rule: this CLI cannot tell them apart at the point of rendering.
|
|
1775
|
+
*
|
|
1776
|
+
* The `network` and `malformed` details are Node's own (a DNS failure, a `JSON.parse` message),
|
|
1777
|
+
* not the server's, so they stay. The `http` case is the one that used to interpolate
|
|
1778
|
+
* `failure.wireMessage`, and that is exactly the string a handler builds from `str(exc)`.
|
|
1779
|
+
*/
|
|
1780
|
+
declare function describeFailure(failure: ApiFailure): string;
|
|
1781
|
+
/** The detail lines a command appends under `describeFailure`. */
|
|
1782
|
+
declare function failureDetails(failure: ApiFailure): readonly string[];
|
|
1783
|
+
|
|
1784
|
+
declare function failureOutcome(failure: ApiFailure, extraDetails?: readonly string[]): CommandOutcome;
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Narrow `GET /widget/v1/diagnostics` into the CLI's own shape.
|
|
1788
|
+
*
|
|
1789
|
+
* Two rules, both deliberate:
|
|
1790
|
+
*
|
|
1791
|
+
* 1. STRUCTURAL fields are required. A body with no `capabilities` array is not a diagnosis,
|
|
1792
|
+
* and pretending it is would print a confident "0 capabilities" for what is actually a
|
|
1793
|
+
* proxy error page. Those cases surface as `malformed_response`, exit 6.
|
|
1794
|
+
* 2. VALUE fields are tolerated. A new reason code, a new lattice kind, or an added field is
|
|
1795
|
+
* the platform growing, and this CLI must keep answering across that. Unknown reason codes
|
|
1796
|
+
* are carried through verbatim and get the upgrade next-action.
|
|
1797
|
+
*
|
|
1798
|
+
* Field names are snake_case on the wire and camelCase here, converted once, at this boundary.
|
|
1799
|
+
*/
|
|
1800
|
+
interface CapabilityDiagnosis {
|
|
1801
|
+
readonly key: string;
|
|
1802
|
+
readonly available: boolean;
|
|
1803
|
+
readonly reasonCode: string;
|
|
1804
|
+
readonly reason: string;
|
|
1805
|
+
readonly entitlementReasonCode: string;
|
|
1806
|
+
readonly decidedAtLevel: number | null;
|
|
1807
|
+
readonly decidedAtKind: string | null;
|
|
1808
|
+
readonly surface: string | null;
|
|
1809
|
+
/** CLI-authored. What the partner should do; see `reasons.ts`. */
|
|
1810
|
+
readonly nextAction: string;
|
|
1811
|
+
}
|
|
1812
|
+
interface ScopeLevelDiagnosis {
|
|
1813
|
+
readonly level: number;
|
|
1814
|
+
readonly kind: string;
|
|
1815
|
+
readonly present: boolean;
|
|
1816
|
+
}
|
|
1817
|
+
interface Diagnosis {
|
|
1818
|
+
readonly requestId: string | null;
|
|
1819
|
+
readonly evaluatedAt: string;
|
|
1820
|
+
readonly clientId: string;
|
|
1821
|
+
readonly clientSlug: string;
|
|
1822
|
+
readonly auth: {
|
|
1823
|
+
readonly mode: string;
|
|
1824
|
+
readonly anonymous: boolean;
|
|
1825
|
+
readonly userRefPrefix: string;
|
|
1826
|
+
readonly userRole: string;
|
|
1827
|
+
readonly roleSource: string;
|
|
1828
|
+
readonly sessionId: string;
|
|
1829
|
+
readonly partnerKeyScopeResolved: boolean;
|
|
1830
|
+
};
|
|
1831
|
+
readonly scope: {
|
|
1832
|
+
readonly provisioned: boolean;
|
|
1833
|
+
readonly resolverStatus: string;
|
|
1834
|
+
readonly revision: number;
|
|
1835
|
+
readonly region: string;
|
|
1836
|
+
readonly levels: readonly ScopeLevelDiagnosis[];
|
|
1837
|
+
};
|
|
1838
|
+
readonly capabilities: readonly CapabilityDiagnosis[];
|
|
1839
|
+
}
|
|
1840
|
+
type DiagnosisParse = {
|
|
1841
|
+
readonly ok: true;
|
|
1842
|
+
readonly diagnosis: Diagnosis;
|
|
1843
|
+
} | {
|
|
1844
|
+
readonly ok: false;
|
|
1845
|
+
readonly detail: string;
|
|
1846
|
+
};
|
|
1847
|
+
declare function parseDiagnostics(body: unknown, headerRequestId: string | null): DiagnosisParse;
|
|
1848
|
+
|
|
1849
|
+
interface DiagnosisDocumentInput {
|
|
1850
|
+
readonly diagnosis: Diagnosis;
|
|
1851
|
+
readonly baseUrl: string;
|
|
1852
|
+
readonly path: string;
|
|
1853
|
+
readonly cliVersion: string;
|
|
1854
|
+
}
|
|
1855
|
+
declare function buildDiagnosisDocument(input: DiagnosisDocumentInput): JsonObject;
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* The human projection of a diagnosis. Same facts as the JSON, same order, nothing added.
|
|
1859
|
+
*
|
|
1860
|
+
* The unavailable list is GROUPED BY CAUSE rather than printed key by key. That is the whole
|
|
1861
|
+
* point of the command: a partner staring at eight unavailable capabilities needs to see that
|
|
1862
|
+
* they are eight symptoms of one unprovisioned scope, not eight problems. Each group prints its
|
|
1863
|
+
* machine code once, the server's copy once, and the next action once.
|
|
1864
|
+
*/
|
|
1865
|
+
interface DiagnosisRenderInput {
|
|
1866
|
+
readonly diagnosis: Diagnosis;
|
|
1867
|
+
readonly baseUrl: string;
|
|
1868
|
+
}
|
|
1869
|
+
declare function renderDiagnosis(input: DiagnosisRenderInput): string;
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* WHY A CAPABILITY IS UNAVAILABLE, and what to do about it.
|
|
1873
|
+
*
|
|
1874
|
+
* The nine codes are the platform's own `DiagnosticsReasonCode`, which exists because an empty
|
|
1875
|
+
* `capabilities:{}` verdict has five byte-identical causes and the render payload deliberately
|
|
1876
|
+
* carries no "why". The server sends the code and its own safe copy; this table adds the part
|
|
1877
|
+
* the server cannot know, which is what the PARTNER should do next.
|
|
1878
|
+
*
|
|
1879
|
+
* The distinction that makes this table worth having: three of these are somebody else's job
|
|
1880
|
+
* (`scope_not_provisioned`, `not_in_grant`, `runtime_gate_off`), one is a commercial
|
|
1881
|
+
* conversation (`outside_ceiling`), one is deliberate configuration (`capability_denied`), one
|
|
1882
|
+
* is transient (`resolver_unavailable`), and one is a bug to report (`gate_unknown`). A single
|
|
1883
|
+
* "the feature is unavailable" message collapses all seven into one wrong next step.
|
|
1884
|
+
*
|
|
1885
|
+
* An UNKNOWN code is not an error. The enum grows when the platform grows a new decision point,
|
|
1886
|
+
* and a CLI that rejected the answer would be useless exactly when something new broke. It
|
|
1887
|
+
* renders the code verbatim with an upgrade instruction.
|
|
1888
|
+
*/
|
|
1889
|
+
declare const REASON_CODES: readonly ["granted", "scope_not_provisioned", "capability_denied", "outside_ceiling", "not_in_grant", "runtime_gate_off", "gate_unknown", "platform_invariant", "resolver_unavailable"];
|
|
1890
|
+
type ReasonCode = (typeof REASON_CODES)[number];
|
|
1891
|
+
declare function isKnownReasonCode(value: string): value is ReasonCode;
|
|
1892
|
+
/** The next action for a code, including one this CLI has never heard of. */
|
|
1893
|
+
declare function nextActionFor$1(code: string): string;
|
|
1894
|
+
|
|
1895
|
+
/**
|
|
1896
|
+
* THE SECRET FENCE. This module turns an admin mint response into the only shape this CLI is
|
|
1897
|
+
* allowed to hand back, and it is written so that a raw secret CANNOT travel through it.
|
|
1898
|
+
*
|
|
1899
|
+
* The rule, stated once and enforced by construction rather than by care:
|
|
1900
|
+
*
|
|
1901
|
+
* The projection reads a FIXED LIST OF FIELD NAMES off the response. There is no spread, no
|
|
1902
|
+
* `Object.entries` loop, no "copy everything except". `secret_value` is not in the list, so
|
|
1903
|
+
* no future field added to the response can arrive here by accident either.
|
|
1904
|
+
*
|
|
1905
|
+
* That mirrors the reasoning in the platform's own `_metadata` helper, which forbids a
|
|
1906
|
+
* `**record` splat for the same reason: the one field that must never reach a response is
|
|
1907
|
+
* exactly the field a splat would carry.
|
|
1908
|
+
*
|
|
1909
|
+
* THE SERVER-SIDE GAP. The approved design has this command return `{key_id, claim_url,
|
|
1910
|
+
* claim_expires_at}` and a human claiming the secret in a browser. The live route
|
|
1911
|
+
* (`POST /api/v1/admin/clients/{client_id}/secret-keys`) returns `secret_value` inline and no
|
|
1912
|
+
* claim URL at all: there is no claim flow on the server yet. This client models the approved
|
|
1913
|
+
* design, reads a `claim_url` when one appears, and reports `claim_unavailable` when it does
|
|
1914
|
+
* not. It never falls back to printing what it was handed. See DECISIONS.md, open question 6.
|
|
1915
|
+
*
|
|
1916
|
+
* AND IT CLEANS UP AFTER ITSELF. Discarding the secret is only half an answer: the key is
|
|
1917
|
+
* still live server side, so a command that stopped there would leave a credential nobody
|
|
1918
|
+
* holds behind on EVERY invocation against the live route, and then print an instruction
|
|
1919
|
+
* telling the operator to go and revoke it by hand. That is worse than the stub the registry's
|
|
1920
|
+
* own doctrine forbids, because it has a side effect. So `claim_unavailable` now REVOKES the
|
|
1921
|
+
* key it just minted, through the route's own idempotent
|
|
1922
|
+
* `DELETE /admin/clients/{client_id}/secret-keys/{key_id}`, and reports whether that worked.
|
|
1923
|
+
* The operator is only ever asked to revoke by hand when the revoke itself failed.
|
|
1924
|
+
*/
|
|
1925
|
+
type SandboxCreateStatus = 'claim_issued' | 'claim_unavailable';
|
|
1926
|
+
/**
|
|
1927
|
+
* What happened to the key on the `claim_unavailable` path.
|
|
1928
|
+
*
|
|
1929
|
+
* not_needed a claim URL was issued, so the key is wanted and stays.
|
|
1930
|
+
* revoked the mint produced an unclaimable key and this command revoked it.
|
|
1931
|
+
* revoke_failed the revoke was attempted and did not succeed. THIS is the only case where a
|
|
1932
|
+
* live orphaned credential is left behind, and the only case where the report
|
|
1933
|
+
* asks a human to remove it.
|
|
1934
|
+
*/
|
|
1935
|
+
type SandboxRevocation = 'not_needed' | 'revoked' | 'revoke_failed';
|
|
1936
|
+
interface SandboxCreation {
|
|
1937
|
+
readonly status: SandboxCreateStatus;
|
|
1938
|
+
readonly keyId: string;
|
|
1939
|
+
readonly clientId: string;
|
|
1940
|
+
readonly label: string | null;
|
|
1941
|
+
readonly createdAt: string | null;
|
|
1942
|
+
readonly claimUrl: string | null;
|
|
1943
|
+
readonly claimExpiresAt: string | null;
|
|
1944
|
+
/**
|
|
1945
|
+
* True when the response carried a raw secret that this CLI discarded unread. Reported
|
|
1946
|
+
* because it is operationally important: a credential now exists that nobody holds.
|
|
1947
|
+
*/
|
|
1948
|
+
readonly secretDiscarded: boolean;
|
|
1949
|
+
}
|
|
1950
|
+
type SandboxCreateParse = {
|
|
1951
|
+
readonly ok: true;
|
|
1952
|
+
readonly creation: SandboxCreation;
|
|
1953
|
+
} | {
|
|
1954
|
+
readonly ok: false;
|
|
1955
|
+
readonly detail: string;
|
|
1956
|
+
};
|
|
1957
|
+
/**
|
|
1958
|
+
* Project the mint response. Every field is named; nothing is copied wholesale.
|
|
1959
|
+
*
|
|
1960
|
+
* `secretDiscarded` is derived by TESTING FOR PRESENCE ONLY. The value is never bound to a
|
|
1961
|
+
* variable, never returned, and never reaches a string, which is why a `--json` document and a
|
|
1962
|
+
* rendered report are both provably free of it whatever the server sent.
|
|
1963
|
+
*/
|
|
1964
|
+
declare function projectSandboxCreate(body: unknown): SandboxCreateParse;
|
|
1965
|
+
interface SandboxDocumentInput {
|
|
1966
|
+
readonly creation: SandboxCreation;
|
|
1967
|
+
readonly revocation: SandboxRevocation;
|
|
1968
|
+
readonly requestId: string | null;
|
|
1969
|
+
readonly cliVersion: string;
|
|
1970
|
+
}
|
|
1971
|
+
/** The next actions for each status. Never contains a value, only names and commands. */
|
|
1972
|
+
declare function nextActionsFor(creation: SandboxCreation, revocation: SandboxRevocation): readonly string[];
|
|
1973
|
+
declare function buildSandboxDocument(input: SandboxDocumentInput): JsonObject;
|
|
1974
|
+
/** The human projection. */
|
|
1975
|
+
declare function renderSandboxCreate(creation: SandboxCreation, revocation: SandboxRevocation, requestId: string | null): string;
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* THE WIRE EXERCISE. Three steps, run in order, each of which reports what happened rather than
|
|
1979
|
+
* what the process is about to exit with.
|
|
1980
|
+
*
|
|
1981
|
+
* mint-session POST /widget/v1/sessions with the sk + pk pair.
|
|
1982
|
+
* poll-entitlements GET /widget/v1/entitlements with the minted bearer.
|
|
1983
|
+
* assert-verdict the verdict carries at least one capability.
|
|
1984
|
+
*
|
|
1985
|
+
* Three properties this file exists to guarantee:
|
|
1986
|
+
*
|
|
1987
|
+
* 1. EVERY STEP CARRIES A REASON, including a passing one. "PASS" alone tells an operator
|
|
1988
|
+
* nothing they can paste into a ticket; "PASS: minted a session valid until ..." does.
|
|
1989
|
+
* 2. A STEP THAT COULD NOT RUN IS SKIP, NOT FAIL. Missing credentials are not a broken
|
|
1990
|
+
* integration, and reporting them as failures would send someone debugging the API when
|
|
1991
|
+
* they simply have not exported a key yet.
|
|
1992
|
+
* 3. THE MINTED TOKEN NEVER REACHES A REPORT. It is wrapped as an ephemeral credential the
|
|
1993
|
+
* moment it is read, so the only thing that can travel into step two is a header the HTTP
|
|
1994
|
+
* client writes, and the only thing that can travel into the report is its expiry.
|
|
1995
|
+
*
|
|
1996
|
+
* The mint is a POST and gets exactly one attempt. The poll is a GET, so repeating it is safe,
|
|
1997
|
+
* and it repeats on purpose: an entitlement scope provisioned seconds ago propagates on a
|
|
1998
|
+
* revision bump, and a single read would report a stale empty verdict as a failure.
|
|
1999
|
+
*/
|
|
2000
|
+
type StepResult = 'PASS' | 'FAIL' | 'SKIP';
|
|
2001
|
+
interface VerifyStep {
|
|
2002
|
+
/** Stable within a schemaVersion. Callers may key on it. */
|
|
2003
|
+
readonly id: string;
|
|
2004
|
+
readonly title: string;
|
|
2005
|
+
readonly result: StepResult;
|
|
2006
|
+
/** Always populated, for every result including PASS. */
|
|
2007
|
+
readonly reason: string;
|
|
2008
|
+
readonly requestId: string | null;
|
|
2009
|
+
readonly details: readonly string[];
|
|
2010
|
+
}
|
|
2011
|
+
interface VerifyOptions {
|
|
2012
|
+
readonly client: ApiClient;
|
|
2013
|
+
readonly clock: ClockPort;
|
|
2014
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
2015
|
+
readonly userRef: string;
|
|
2016
|
+
/** How many times to read the verdict before concluding it is empty. */
|
|
2017
|
+
readonly attempts: number;
|
|
2018
|
+
readonly pollDelayMs: number;
|
|
2019
|
+
}
|
|
2020
|
+
interface Verification {
|
|
2021
|
+
readonly steps: readonly VerifyStep[];
|
|
2022
|
+
readonly status: 'passed' | 'failed' | 'incomplete';
|
|
2023
|
+
}
|
|
2024
|
+
declare function runVerification(options: VerifyOptions): Promise<Verification>;
|
|
2025
|
+
|
|
2026
|
+
declare function buildVerificationDocument(verification: Verification, baseUrl: string, cliVersion: string): JsonObject;
|
|
2027
|
+
declare function renderVerification(verification: Verification, baseUrl: string): string;
|
|
2028
|
+
|
|
2029
|
+
declare function nextActionFor(entry: ErrorRegistryEntry): string;
|
|
2030
|
+
interface ExplanationInput {
|
|
2031
|
+
readonly entry: ErrorRegistryEntry;
|
|
2032
|
+
readonly contractPath: string;
|
|
2033
|
+
readonly digest: string;
|
|
2034
|
+
readonly cliVersion: string;
|
|
2035
|
+
}
|
|
2036
|
+
declare function buildExplanationDocument(input: ExplanationInput): JsonObject;
|
|
2037
|
+
declare function renderExplanation(input: ExplanationInput): string;
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Suggestions for a code the registry does not list.
|
|
2041
|
+
*
|
|
2042
|
+
* Worth the twenty lines because the registry deliberately keeps THREE naming families at once
|
|
2043
|
+
* (`AuthorizationException`, `authExpired`, `session_required`), so a partner guessing at a code
|
|
2044
|
+
* guesses in the wrong family constantly, and the codes are case sensitive because a client
|
|
2045
|
+
* branches on the literal. "Did you mean authExpired" is the whole answer in that case.
|
|
2046
|
+
*
|
|
2047
|
+
* Ranked: an exact case-insensitive hit first, then a containment hit, then a near miss by edit
|
|
2048
|
+
* distance. Capped, because a list of twenty guesses is not a suggestion.
|
|
2049
|
+
*/
|
|
2050
|
+
/** Iterative Levenshtein over two rows. No dependency for something this small. */
|
|
2051
|
+
declare function editDistance(left: string, right: string): number;
|
|
2052
|
+
declare function suggestCodes(query: string, codes: readonly string[]): readonly string[];
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* The one real filesystem adapter. Every syscall failure collapses to a null or false, so no
|
|
2056
|
+
* EACCES on an unrelated probe can take the whole command down; a genuinely required file
|
|
2057
|
+
* that reads as null surfaces as a typed `file_not_found` error at the call site instead.
|
|
2058
|
+
*/
|
|
2059
|
+
declare const nodeFileSystem: FileSystemPort;
|
|
2060
|
+
|
|
2061
|
+
/**
|
|
2062
|
+
* An in-memory FileSystemPort for tests that need a shape no fixture on disk should carry,
|
|
2063
|
+
* for example two competing lockfiles or a manifest that is not valid JSON. Fixture projects
|
|
2064
|
+
* on disk stay the primary detection test surface; this covers the pathological cases.
|
|
2065
|
+
*
|
|
2066
|
+
* Directories are implied by the file keys, so a caller only ever declares files.
|
|
2067
|
+
*/
|
|
2068
|
+
declare function createMemoryFileSystem(files: Readonly<Record<string, string>>): FileSystemPort;
|
|
2069
|
+
|
|
2070
|
+
/**
|
|
2071
|
+
* The detection vocabulary. Every value here is something the CLI can establish from files on
|
|
2072
|
+
* disk. Anything it cannot establish lives in `src/commands/plan/facts.ts` as a supplied fact,
|
|
2073
|
+
* and the two sets are deliberately disjoint.
|
|
2074
|
+
*/
|
|
2075
|
+
type ProjectType = 'expo-managed' | 'bare-react-native' | 'flutter' | 'web' | 'unknown';
|
|
2076
|
+
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'not-applicable' | 'undetermined';
|
|
2077
|
+
type DependencySource = 'package.json' | 'pubspec.yaml';
|
|
2078
|
+
type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'sdk';
|
|
2079
|
+
/**
|
|
2080
|
+
* `declared` is the range written in the manifest. `installed` is the exact version resolved
|
|
2081
|
+
* on disk when the dependency tree is present. Reporting both is the honest answer: a plan
|
|
2082
|
+
* built against `^50.0.0` with nothing installed is materially less certain than one built
|
|
2083
|
+
* against a resolved 50.0.17, and a reader deserves to see which of the two they are getting.
|
|
2084
|
+
*/
|
|
2085
|
+
interface DetectedDependency {
|
|
2086
|
+
readonly name: string;
|
|
2087
|
+
readonly declared: string | null;
|
|
2088
|
+
readonly installed: string | null;
|
|
2089
|
+
readonly source: DependencySource;
|
|
2090
|
+
readonly scope: DependencyScope;
|
|
2091
|
+
}
|
|
2092
|
+
interface ProjectDetection {
|
|
2093
|
+
readonly root: string;
|
|
2094
|
+
readonly projectType: ProjectType;
|
|
2095
|
+
/** Human-readable file-level facts that produced `projectType`, in the order they were found. */
|
|
2096
|
+
readonly projectTypeEvidence: readonly string[];
|
|
2097
|
+
readonly packageManager: PackageManager;
|
|
2098
|
+
readonly packageManagerEvidence: readonly string[];
|
|
2099
|
+
readonly dependencies: readonly DetectedDependency[];
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
declare function detectProject(fs: FileSystemPort, root: string): ProjectDetection;
|
|
2103
|
+
|
|
2104
|
+
type AttributeValue = string | number | boolean | null | readonly string[];
|
|
2105
|
+
interface TreePrinciple {
|
|
2106
|
+
readonly id: string;
|
|
2107
|
+
readonly title: string;
|
|
2108
|
+
readonly statement: string;
|
|
2109
|
+
readonly docPath: string | null;
|
|
2110
|
+
}
|
|
2111
|
+
interface BranchOutcome {
|
|
2112
|
+
readonly kind: 'branch';
|
|
2113
|
+
readonly nextQuestionId: string;
|
|
2114
|
+
}
|
|
2115
|
+
interface TerminalOutcome {
|
|
2116
|
+
readonly kind: 'terminal';
|
|
2117
|
+
readonly attributes: Readonly<Record<string, AttributeValue>>;
|
|
2118
|
+
readonly docPaths: readonly string[];
|
|
2119
|
+
readonly capabilityRefs: readonly string[];
|
|
2120
|
+
/** The tree that must be answered next, or null when this answer stands alone. */
|
|
2121
|
+
readonly thenAskTreeId: string | null;
|
|
2122
|
+
}
|
|
2123
|
+
interface BlockedOutcome {
|
|
2124
|
+
readonly kind: 'blocked';
|
|
2125
|
+
readonly reason: string;
|
|
2126
|
+
readonly unblockOwner: string;
|
|
2127
|
+
readonly requestPath: string;
|
|
2128
|
+
readonly capabilityRefs: readonly string[];
|
|
2129
|
+
}
|
|
2130
|
+
type Outcome = BranchOutcome | TerminalOutcome | BlockedOutcome;
|
|
2131
|
+
interface TreeOption {
|
|
2132
|
+
readonly id: string;
|
|
2133
|
+
readonly label: string;
|
|
2134
|
+
readonly when: string;
|
|
2135
|
+
readonly outcome: Outcome;
|
|
2136
|
+
}
|
|
2137
|
+
interface TreeQuestion {
|
|
2138
|
+
readonly id: string;
|
|
2139
|
+
readonly prompt: string;
|
|
2140
|
+
readonly whyItIsAsked: string;
|
|
2141
|
+
/**
|
|
2142
|
+
* The exact invocation change that answers this question, or null when there is none. Null is
|
|
2143
|
+
* an answer, not an omission: a question whose only option is implied has no flag.
|
|
2144
|
+
*/
|
|
2145
|
+
readonly supplyWith: string | null;
|
|
2146
|
+
readonly detectable: boolean;
|
|
2147
|
+
/** Sorted by id, because the published order is alphabetical and means nothing. */
|
|
2148
|
+
readonly options: readonly TreeOption[];
|
|
2149
|
+
}
|
|
2150
|
+
interface DecisionTree {
|
|
2151
|
+
readonly treeId: string;
|
|
2152
|
+
readonly title: string;
|
|
2153
|
+
readonly decides: string;
|
|
2154
|
+
/** The exact invocation change that answers this tree. Rendered in a refusal. */
|
|
2155
|
+
readonly supplyWith: string;
|
|
2156
|
+
readonly entryQuestionId: string;
|
|
2157
|
+
readonly principles: readonly TreePrinciple[];
|
|
2158
|
+
readonly questions: readonly TreeQuestion[];
|
|
2159
|
+
}
|
|
2160
|
+
interface DecisionSpace {
|
|
2161
|
+
readonly trees: readonly DecisionTree[];
|
|
2162
|
+
/** Where it came from and what it hashed to, carried into the plan document. */
|
|
2163
|
+
readonly source: string;
|
|
2164
|
+
readonly digest: string;
|
|
2165
|
+
tree(treeId: string): DecisionTree | null;
|
|
2166
|
+
question(treeId: string, questionId: string): TreeQuestion | null;
|
|
2167
|
+
/** The option ids of one question, sorted. The enum a flag is parsed against. */
|
|
2168
|
+
choices(treeId: string, questionId: string): readonly string[];
|
|
2169
|
+
}
|
|
2170
|
+
declare function buildDecisionSpace(trees: readonly DecisionTree[], source: string, digest: string): DecisionSpace;
|
|
2171
|
+
type DecisionSpaceLoad = {
|
|
2172
|
+
readonly kind: 'loaded';
|
|
2173
|
+
readonly space: DecisionSpace;
|
|
2174
|
+
} | {
|
|
2175
|
+
readonly kind: 'no_trees';
|
|
2176
|
+
readonly path: string;
|
|
2177
|
+
} | {
|
|
2178
|
+
readonly kind: 'contract_rejected';
|
|
2179
|
+
readonly outcome: ContractValidationOutcome;
|
|
2180
|
+
};
|
|
2181
|
+
/**
|
|
2182
|
+
* Load the decision space out of a contract-public envelope on disk. Contract failures are
|
|
2183
|
+
* handed back unchanged so the caller can reuse `contract validate`'s exact error rendering
|
|
2184
|
+
* rather than inventing a second, subtly different set of messages for the same three failures.
|
|
2185
|
+
*/
|
|
2186
|
+
declare function loadDecisionSpace(fs: FileSystemPort, path: string): DecisionSpaceLoad;
|
|
2187
|
+
/** One question the walk passed through, and the option it took. */
|
|
2188
|
+
interface WalkStep {
|
|
2189
|
+
readonly questionId: string;
|
|
2190
|
+
readonly optionId: string;
|
|
2191
|
+
/** True when the question had exactly one option, so nothing was chosen. */
|
|
2192
|
+
readonly implied: boolean;
|
|
2193
|
+
}
|
|
2194
|
+
type WalkResult = {
|
|
2195
|
+
readonly kind: 'terminal';
|
|
2196
|
+
readonly outcome: TerminalOutcome;
|
|
2197
|
+
readonly path: readonly WalkStep[];
|
|
2198
|
+
} | {
|
|
2199
|
+
readonly kind: 'blocked';
|
|
2200
|
+
readonly outcome: BlockedOutcome;
|
|
2201
|
+
readonly path: readonly WalkStep[];
|
|
2202
|
+
} | {
|
|
2203
|
+
readonly kind: 'unanswered';
|
|
2204
|
+
readonly question: TreeQuestion;
|
|
2205
|
+
} | {
|
|
2206
|
+
readonly kind: 'unknown_option';
|
|
2207
|
+
readonly question: TreeQuestion;
|
|
2208
|
+
readonly given: string;
|
|
2209
|
+
} | {
|
|
2210
|
+
readonly kind: 'unwalkable';
|
|
2211
|
+
readonly detail: string;
|
|
2212
|
+
};
|
|
2213
|
+
/** Supplies the answer to one question, or null when the caller has not answered it. */
|
|
2214
|
+
type AnswerFor = (questionId: string) => string | null;
|
|
2215
|
+
/**
|
|
2216
|
+
* Walk one tree. Terminates on a terminal, a block, or the first question nobody answered.
|
|
2217
|
+
*
|
|
2218
|
+
* A QUESTION WITH EXACTLY ONE OPTION IS TAKEN WITHOUT AN ANSWER, and the step records that it
|
|
2219
|
+
* was implied. One option is not a decision: it is the contract saying "everyone who gets here
|
|
2220
|
+
* ends up in the same place", which is how three backend languages with no first-party minter
|
|
2221
|
+
* converge on one description of the direct call. Demanding a flag for it would be demanding
|
|
2222
|
+
* the caller confirm the only thing that could happen.
|
|
2223
|
+
*
|
|
2224
|
+
* The `visited` guard is not a substitute for the compiler's cycle check; it is what keeps a
|
|
2225
|
+
* projection built by something other than that compiler from hanging this process.
|
|
2226
|
+
*/
|
|
2227
|
+
declare function walkTree(tree: DecisionTree, answerFor: AnswerFor): WalkResult;
|
|
2228
|
+
/** Walk a tree whose whole answer is one option of its entry question. */
|
|
2229
|
+
declare function walkSingle(tree: DecisionTree, answer: string): WalkResult;
|
|
2230
|
+
|
|
2231
|
+
/** One file a plan tells the caller to write. */
|
|
2232
|
+
interface PlanCode {
|
|
2233
|
+
/** Suggested path, relative to the project root. */
|
|
2234
|
+
readonly path: string;
|
|
2235
|
+
/** For syntax highlighting and for an agent deciding how to write it. */
|
|
2236
|
+
readonly language: 'tsx' | 'ts' | 'dart';
|
|
2237
|
+
readonly contents: string;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
/**
|
|
2241
|
+
* THE PLAN CONTRACT.
|
|
2242
|
+
*
|
|
2243
|
+
* This file and `schema/plan.v1.schema.json` are two views of one committed artifact. Anything
|
|
2244
|
+
* a caller can parse out of `everfur plan --json` is declared here, and `tests/contract`
|
|
2245
|
+
* validates real emitted output against the JSON Schema on every run, so the two cannot drift
|
|
2246
|
+
* without the suite going red.
|
|
2247
|
+
*
|
|
2248
|
+
* Compatibility rule for `schemaVersion: 1`: fields may be ADDED, and enum members may be
|
|
2249
|
+
* ADDED. A field is never removed, never renamed, and never changes type or meaning. Any of
|
|
2250
|
+
* those is `schemaVersion: 2` and a new schema file beside this one.
|
|
2251
|
+
*
|
|
2252
|
+
* WHAT CHANGED WHEN THE DECISION TREES LANDED, and why it stayed v1:
|
|
2253
|
+
*
|
|
2254
|
+
* - `status` gained `blocked`, `missingFacts[].fact` gained `decisionTrees` and
|
|
2255
|
+
* `sessionMintLanguage`: added enum members, which v1 permits.
|
|
2256
|
+
* - `blocked`, `principles` and `decisionSpace` are new fields, which v1 permits.
|
|
2257
|
+
* - `Capability` and `IdentityMode` stopped being enums and became patterned strings. This is
|
|
2258
|
+
* the one change worth arguing about, so it is argued here: the set was never this package's
|
|
2259
|
+
* to own. It lives in `contract/decision-trees/`, and an enum here would reject a capability
|
|
2260
|
+
* the platform published until somebody remembered to edit a CLI. The relaxation accepts
|
|
2261
|
+
* every document v1 already accepted and rejects nothing it used to, which is the test that
|
|
2262
|
+
* keeps it inside v1.
|
|
2263
|
+
*/
|
|
2264
|
+
declare const PLAN_SCHEMA_VERSION = 1;
|
|
2265
|
+
declare const PLAN_DOCUMENT_KIND = "everfur.plan";
|
|
2266
|
+
/**
|
|
2267
|
+
* A capability id: one option of the capabilities tree's entry question. NOT an enum here.
|
|
2268
|
+
* The authoritative list is the contract's, and `everfur plan` reads it from the projection.
|
|
2269
|
+
*/
|
|
2270
|
+
type Capability = string;
|
|
2271
|
+
/** An identity mode id: one option of the identity-mode tree's entry question. */
|
|
2272
|
+
type IdentityMode = string;
|
|
2273
|
+
/** A backend language id: one option of the session-mint-language tree's entry question. */
|
|
2274
|
+
type SessionMintLanguage = string;
|
|
2275
|
+
type PlanStatus = 'ready' | 'underspecified' | 'blocked';
|
|
2276
|
+
/**
|
|
2277
|
+
* The facts `plan` needs. Each is either detected from disk, supplied by flag, or loaded from
|
|
2278
|
+
* the contract, and never guessed.
|
|
2279
|
+
*
|
|
2280
|
+
* `decisionTrees` comes first because it is the fact the others are judged against: with no
|
|
2281
|
+
* decision space the CLI cannot say whether a capability exists, let alone whether it is
|
|
2282
|
+
* reachable. It is a REFUSAL rather than a broken-artifact failure because nothing was loaded
|
|
2283
|
+
* and nothing on disk is wrong; the operator simply has not said where the contract is.
|
|
2284
|
+
*/
|
|
2285
|
+
declare const MISSING_FACT_NAMES: readonly ["decisionTrees", "projectType", "packageManager", "capabilities", "identityMode", "sessionMintLanguage"];
|
|
2286
|
+
type MissingFactName = (typeof MISSING_FACT_NAMES)[number];
|
|
2287
|
+
interface MissingFact {
|
|
2288
|
+
readonly fact: MissingFactName;
|
|
2289
|
+
/** Why the CLI could not establish it, phrased as what it looked at. */
|
|
2290
|
+
readonly reason: string;
|
|
2291
|
+
/** The exact invocation change that resolves it. */
|
|
2292
|
+
readonly supplyWith: string;
|
|
2293
|
+
}
|
|
2294
|
+
type PlanPhase = 'install' | 'native' | 'configure' | 'code' | 'verify';
|
|
2295
|
+
interface PlanStep {
|
|
2296
|
+
/** Stable within a schemaVersion. Callers may key on it. */
|
|
2297
|
+
readonly id: string;
|
|
2298
|
+
readonly phase: PlanPhase;
|
|
2299
|
+
readonly title: string;
|
|
2300
|
+
readonly detail: string;
|
|
2301
|
+
/** Shell commands, in order. Never contains a credential value. */
|
|
2302
|
+
readonly commands: readonly string[];
|
|
2303
|
+
/** Files the partner is expected to create or edit. */
|
|
2304
|
+
readonly files: readonly string[];
|
|
2305
|
+
/**
|
|
2306
|
+
* Code to write, when this package can name every symbol in it.
|
|
2307
|
+
*
|
|
2308
|
+
* Absent rather than invented when it cannot: a plausible-looking snippet naming a component that
|
|
2309
|
+
* does not exist is worse than no snippet, because it compiles in the reader's head and fails in
|
|
2310
|
+
* their editor. See codegen.ts for which surfaces qualify.
|
|
2311
|
+
*/
|
|
2312
|
+
readonly code?: readonly PlanCode[];
|
|
2313
|
+
}
|
|
2314
|
+
interface PlanWarning {
|
|
2315
|
+
readonly code: string;
|
|
2316
|
+
readonly message: string;
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* A capability the caller asked for that a planner must not build against, lifted verbatim from
|
|
2320
|
+
* a BLOCKED terminal in the contract.
|
|
2321
|
+
*
|
|
2322
|
+
* This is the field that stops the CLI producing a confident, wrong plan. Without it, a
|
|
2323
|
+
* capability whose runtime gate is a control-plane switch reads as ordinary: the plan installs
|
|
2324
|
+
* the SDK, mounts the surface, and every route answers 404 in production, which the partner
|
|
2325
|
+
* reads as a broken SDK rather than as a switch nobody turned on.
|
|
2326
|
+
*/
|
|
2327
|
+
interface BlockedCapability {
|
|
2328
|
+
/** The capability id the caller named. */
|
|
2329
|
+
readonly capability: string;
|
|
2330
|
+
readonly reason: string;
|
|
2331
|
+
readonly unblockOwner: string;
|
|
2332
|
+
/** Site-relative path on the partner documentation site. Resolved at compile time. */
|
|
2333
|
+
readonly requestPath: string;
|
|
2334
|
+
/** The capability pack keys the block derives from. */
|
|
2335
|
+
readonly capabilityKeys: readonly string[];
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Guidance that is true regardless of what was answered, carried in the document rather than
|
|
2339
|
+
* left in a prompt. An agent that has compacted its context away still holds these.
|
|
2340
|
+
*/
|
|
2341
|
+
interface PlanPrinciple {
|
|
2342
|
+
readonly treeId: string;
|
|
2343
|
+
readonly id: string;
|
|
2344
|
+
readonly title: string;
|
|
2345
|
+
readonly statement: string;
|
|
2346
|
+
readonly docPath: string | null;
|
|
2347
|
+
}
|
|
2348
|
+
/** Which decision space produced this plan, so a stale plan can be traced to a stale contract. */
|
|
2349
|
+
interface DecisionSpaceProvenance {
|
|
2350
|
+
readonly source: string;
|
|
2351
|
+
readonly digest: string;
|
|
2352
|
+
readonly trees: readonly string[];
|
|
2353
|
+
}
|
|
2354
|
+
interface PlanDetection {
|
|
2355
|
+
readonly root: string;
|
|
2356
|
+
readonly projectType: ProjectType;
|
|
2357
|
+
readonly projectTypeEvidence: readonly string[];
|
|
2358
|
+
readonly packageManager: PackageManager;
|
|
2359
|
+
readonly packageManagerEvidence: readonly string[];
|
|
2360
|
+
readonly dependencies: readonly {
|
|
2361
|
+
readonly name: string;
|
|
2362
|
+
readonly declared: string | null;
|
|
2363
|
+
readonly installed: string | null;
|
|
2364
|
+
readonly source: string;
|
|
2365
|
+
readonly scope: string;
|
|
2366
|
+
}[];
|
|
2367
|
+
}
|
|
2368
|
+
interface PlanDocument {
|
|
2369
|
+
readonly schemaVersion: typeof PLAN_SCHEMA_VERSION;
|
|
2370
|
+
readonly kind: typeof PLAN_DOCUMENT_KIND;
|
|
2371
|
+
readonly generatedBy: {
|
|
2372
|
+
readonly name: string;
|
|
2373
|
+
readonly version: string;
|
|
2374
|
+
};
|
|
2375
|
+
readonly status: PlanStatus;
|
|
2376
|
+
/** What the CLI established by reading the project. Always populated, refusal included. */
|
|
2377
|
+
readonly detected: PlanDetection;
|
|
2378
|
+
/** What the caller supplied. `null` means the flag was absent. */
|
|
2379
|
+
readonly supplied: {
|
|
2380
|
+
readonly capabilities: readonly Capability[] | null;
|
|
2381
|
+
readonly identityMode: IdentityMode | null;
|
|
2382
|
+
readonly sessionMintLanguage: SessionMintLanguage | null;
|
|
2383
|
+
};
|
|
2384
|
+
/** Empty when `status` is `ready`. Non-empty exactly when it is `underspecified`. */
|
|
2385
|
+
readonly missingFacts: readonly MissingFact[];
|
|
2386
|
+
/** Empty when `status` is `underspecified` or `blocked`. The CLI emits no partial plan. */
|
|
2387
|
+
readonly steps: readonly PlanStep[];
|
|
2388
|
+
/** Non-empty exactly when a selected capability cannot be built today. */
|
|
2389
|
+
readonly blocked: readonly BlockedCapability[];
|
|
2390
|
+
/** The principles of every tree this plan consulted. */
|
|
2391
|
+
readonly principles: readonly PlanPrinciple[];
|
|
2392
|
+
readonly warnings: readonly PlanWarning[];
|
|
2393
|
+
/** Null only on the refusal that says no decision space was found. */
|
|
2394
|
+
readonly decisionSpace: DecisionSpaceProvenance | null;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* The planner. A pure function from (what was detected, what was supplied, what the contract
|
|
2399
|
+
* says) to a plan document. No filesystem, no clock, no network, no environment: the same
|
|
2400
|
+
* inputs always produce a byte-identical document, which is what makes the JSON contract
|
|
2401
|
+
* testable and diffable in CI.
|
|
2402
|
+
*
|
|
2403
|
+
* THE HEADLINE BEHAVIOUR IS STILL THE REFUSAL. When any required fact is absent the planner
|
|
2404
|
+
* returns a complete, schema-valid document with `status: "underspecified"`, an empty `steps`,
|
|
2405
|
+
* and a `missingFacts` entry per absent fact naming both the reason and the exact flag that
|
|
2406
|
+
* supplies it. It does not emit a partial plan, and it never substitutes a default. A wrong
|
|
2407
|
+
* plan that looks right costs a partner a day; a refusal costs them one flag.
|
|
2408
|
+
*
|
|
2409
|
+
* THE SECOND BEHAVIOUR IS THE BLOCK, and it is new. The decision space now comes from the
|
|
2410
|
+
* contract, and the contract can say that a capability is not buildable: its runtime gate is a
|
|
2411
|
+
* control-plane switch that is off, or every action in it is closed to an agent. Before this,
|
|
2412
|
+
* asking for such a capability produced a confident plan whose every route answers 404 in
|
|
2413
|
+
* production, which a partner reads as a broken SDK. Now it produces the reason and the path to
|
|
2414
|
+
* ask, and no steps for that capability at all.
|
|
2415
|
+
*
|
|
2416
|
+
* WHAT THIS FILE NO LONGER DECIDES: which capabilities exist, which identity modes exist, what
|
|
2417
|
+
* a capability's import target is, which optional native peers it binds, which verb installs
|
|
2418
|
+
* into this project, and what to tell somebody who is missing a fact. All of that is read from
|
|
2419
|
+
* the trees. What is left here is composition: turning answers into ordered steps.
|
|
2420
|
+
*
|
|
2421
|
+
* SECRETS. Nothing in this file mints, derives, reads, or prints a credential. Configuration
|
|
2422
|
+
* steps name ENVIRONMENT VARIABLES and never values, and `tests/unit/plan-no-secrets.test.ts`
|
|
2423
|
+
* asserts that over the emitted document for every capability and identity combination.
|
|
2424
|
+
*/
|
|
2425
|
+
interface PlanInput {
|
|
2426
|
+
readonly detection: ProjectDetection;
|
|
2427
|
+
readonly capabilities: readonly Capability[] | null;
|
|
2428
|
+
readonly identityMode: IdentityMode | null;
|
|
2429
|
+
readonly sessionMintLanguage: SessionMintLanguage | null;
|
|
2430
|
+
/** The decision space from the contract projection, or null when none was found. */
|
|
2431
|
+
readonly space: DecisionSpace | null;
|
|
2432
|
+
readonly cliVersion: string;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Compute the missing facts in a fixed order: the contract first, because everything else is
|
|
2436
|
+
* judged against it; then the detected facts, which the operator fixes by pointing the CLI
|
|
2437
|
+
* somewhere else; then the supplied facts, which are fixed by adding a flag.
|
|
2438
|
+
*/
|
|
2439
|
+
declare function missingFactsFor(input: PlanInput): readonly MissingFact[];
|
|
2440
|
+
/**
|
|
2441
|
+
* Build the plan.
|
|
2442
|
+
*
|
|
2443
|
+
* Three outcomes, and the order between them is deliberate:
|
|
2444
|
+
*
|
|
2445
|
+
* 1. `underspecified` when any required fact is absent. Nothing else is decidable, so this
|
|
2446
|
+
* wins even when some capability is also blocked.
|
|
2447
|
+
* 2. `blocked` when everything was answered and NOTHING the caller asked for can be built.
|
|
2448
|
+
* Steps are empty: an install plan for a surface that answers 404 is not a smaller plan,
|
|
2449
|
+
* it is a wrong one.
|
|
2450
|
+
* 3. `ready` otherwise, with the blocked capabilities still reported beside the steps that
|
|
2451
|
+
* cover the rest.
|
|
2452
|
+
*/
|
|
2453
|
+
declare function planIntegration(input: PlanInput): PlanDocument;
|
|
2454
|
+
|
|
2455
|
+
/** Render whichever projection of the plan the status calls for. Total over `status`. */
|
|
2456
|
+
declare function renderPlan(document: PlanDocument): string;
|
|
2457
|
+
|
|
2458
|
+
/**
|
|
2459
|
+
* Parsing for the facts the CLI cannot detect.
|
|
2460
|
+
*
|
|
2461
|
+
* The distinction this file protects: an ABSENT flag is a missing fact, which the planner turns
|
|
2462
|
+
* into a structured refusal. A PRESENT but invalid flag is a usage error, which fails at exit
|
|
2463
|
+
* code 2 before the planner runs. Collapsing the two would let a typo'd `--identity partner`
|
|
2464
|
+
* read as "the operator did not tell us", which is a materially different and much worse report.
|
|
2465
|
+
*
|
|
2466
|
+
* WHAT IS VALID COMES FROM THE CONTRACT. The accepted values are the option ids of one question
|
|
2467
|
+
* of one decision tree, so a capability the platform published is accepted the day the
|
|
2468
|
+
* projection carries it, with no release of this package. When no decision space is available
|
|
2469
|
+
* the flags are parsed but NOT judged: without the trees this CLI does not know what a valid
|
|
2470
|
+
* capability is, and rejecting a value it cannot evaluate would be guessing in the direction
|
|
2471
|
+
* that looks strict rather than the direction that is honest. The refusal names `decisionTrees`
|
|
2472
|
+
* instead, which is the fact that is actually missing.
|
|
2473
|
+
*/
|
|
2474
|
+
interface SuppliedFactsResult {
|
|
2475
|
+
readonly ok: boolean;
|
|
2476
|
+
readonly capabilities: readonly Capability[] | null;
|
|
2477
|
+
readonly identityMode: IdentityMode | null;
|
|
2478
|
+
readonly sessionMintLanguage: SessionMintLanguage | null;
|
|
2479
|
+
readonly errors: readonly string[];
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Parse `--capabilities chat,records`. Order is normalised to sorted order and duplicates
|
|
2483
|
+
* collapse, so two equivalent invocations produce a byte-identical plan document.
|
|
2484
|
+
*/
|
|
2485
|
+
declare function parseCapabilities(raw: string | null, space: DecisionSpace | null): {
|
|
2486
|
+
readonly value: readonly Capability[] | null;
|
|
2487
|
+
readonly errors: readonly string[];
|
|
2488
|
+
};
|
|
2489
|
+
declare function parseIdentityMode(raw: string | null, space: DecisionSpace | null): {
|
|
2490
|
+
readonly value: IdentityMode | null;
|
|
2491
|
+
readonly errors: readonly string[];
|
|
2492
|
+
};
|
|
2493
|
+
/** Parse `--backend node`. */
|
|
2494
|
+
declare function parseSessionMintLanguage(raw: string | null, space: DecisionSpace | null): {
|
|
2495
|
+
readonly value: SessionMintLanguage | null;
|
|
2496
|
+
readonly errors: readonly string[];
|
|
2497
|
+
};
|
|
2498
|
+
/** Parse every supplied fact, accumulating all errors rather than stopping at the first. */
|
|
2499
|
+
declare function parseSuppliedFacts(capabilitiesRaw: string | null, identityRaw: string | null, backendRaw: string | null, space: DecisionSpace | null): SuppliedFactsResult;
|
|
2500
|
+
|
|
2501
|
+
/**
|
|
2502
|
+
* The CLI version, stamped into every emitted document under `generatedBy.version`.
|
|
2503
|
+
*
|
|
2504
|
+
* Declared here rather than read from package.json at runtime so the bundled bin has no
|
|
2505
|
+
* dependency on its own package layout, which differs between a local checkout, a global
|
|
2506
|
+
* install, and a bundled artifact. `tests/unit/version.test.ts` asserts this constant equals
|
|
2507
|
+
* the package.json version, so the two cannot drift.
|
|
2508
|
+
*/
|
|
2509
|
+
declare const CLI_VERSION = "0.1.0";
|
|
2510
|
+
|
|
2511
|
+
export { ADMIN_TOKEN, API_PREFIX, type ApiClient, type ApiFailure, type ApiResult, type ApiSuccess, BASE_URL_ENV_VAR, type BlockedCapability, type BlockedOutcome, CLI_VERSION, COMMANDS, CREDENTIAL_SPECS, type Capability, type CapabilityDiagnosis, type ClockPort, type Command, type CommandContext, type CommandOutcome, type ContractValidationOutcome, type Credential, type CredentialSpec, DEFAULT_TIMEOUT_MS, type DecisionSpace, type DecisionSpaceProvenance, type DecisionTree, type DetectedDependency, type Diagnosis, EMPTY_REGISTRY, ERRORS_DOCUMENT_KEY, type ErrorRegistry, type ErrorRegistryEntry, ExitCode, type FileSystemPort, type FormatOptions, type FormattedOutput, type HttpMethod, type HttpRequest, type HttpResponse, IDEMPOTENT_METHODS, type IdentityMode, MISSING_FACT_NAMES, type MissingFact, type MissingFactName, type Outcome, PARTNER_KEY, PARTNER_SECRET_KEY, PLAN_DOCUMENT_KIND, PLAN_SCHEMA_VERSION, type PackageManager, type PlanDocument, type PlanInput, type PlanPrinciple, type PlanStep, type PlanWarning, type PositionalSpec, type ProjectDetection, type ProjectType, REASON_CODES, REDACTED, type ReasonCode, type RegistryLoad, type RequestSpec, type ResolvedCredential, type ResolvedEndpoint, type RunOptions, SCHEMAS, SESSION_TOKEN, type SandboxCreateStatus, type SandboxCreation, type SchemaValidation, type SchemaViolation, type SessionMintLanguage, type StepResult, type TerminalOutcome, TransportError, type TransportPort, type TreeOption, type TreeQuestion, type Verification, type VerifyStep, type WalkResult, apiUrl, buildDecisionSpace, buildDiagnosisDocument, buildExplanationDocument, buildSandboxDocument, buildVerificationDocument, clampTimeout, createApiClient, createMemoryFileSystem, createRecordingClock, describeFailure, detectProject, digestOfProjection, digestsEqual, editDistance, entryFromRow, ephemeralCredential, errorCodeOf, failureDetails, failureOutcome, fetchTransport, formatOutcome, isCorrelatableRequestId, isIdempotent, isKnownReasonCode, loadDecisionSpace, loadErrorRegistry, matchCommand, missingFactsFor, nextActionFor as nextActionForErrorCode, nextActionFor$1 as nextActionForReason, nextActionsFor, nodeFileSystem, parseCapabilities, parseDiagnostics, parseIdentityMode, parseSessionMintLanguage, parseSuppliedFacts, planIntegration, projectSandboxCreate, renderDiagnosis, renderExplanation, renderPlan, renderSandboxCreate, renderVerification, resolveContractPath, resolveCredential, resolveEndpoint, runCli, runVerification, suggestCodes, systemClock, validateContractFile, validateContractPublicEnvelope, validateDiagnosisDocument, validateErrorExplanationDocument, validatePlanDocument, validateSandboxCreateDocument, validateVerificationDocument, verifyDigest, walkSingle, walkTree, withPrefix };
|