@vecteur/cli 0.3.0 → 0.4.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/README.md +49 -0
- package/bin/vecteur.js +311 -70
- package/package.json +6 -5
- package/src/client.js +313 -48
- package/src/contract.js +45 -10
- package/src/credentials.js +118 -31
- package/src/download.js +42 -0
- package/src/format.js +147 -8
- package/src/mcp.js +46 -9
- package/src/projects.js +42 -0
- package/bin/vecteur-mcp.js +0 -29
package/src/client.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ARTIFACT_MEDIA_TYPES, EVENT_KINDS, OWNER_CONTRACT_IDENTITY, validateAdmitted,
|
|
5
|
+
validateCustomerCostProjection, validateDeviceAuthorization, validateLimitDenial,
|
|
6
|
+
validatePatCreated, validateProject, validateRunAck, validateAccount, validateQuotaDenial,
|
|
7
|
+
RECOVERY_ACTIONS,
|
|
8
|
+
} from "./contract.js";
|
|
4
9
|
import { readCredentials } from "./credentials.js";
|
|
5
10
|
import { CliError } from "./errors.js";
|
|
6
11
|
import { canonicalJson } from "./format.js";
|
|
@@ -9,12 +14,15 @@ import { canonicalJson } from "./format.js";
|
|
|
9
14
|
export { CliError };
|
|
10
15
|
|
|
11
16
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
17
|
+
export const DEFAULT_ORIGIN = "https://vecteur.space";
|
|
12
18
|
const TOKEN_PATTERN = /vct_[A-Za-z0-9_-]{20,}/g;
|
|
19
|
+
const TOKEN_SHAPE = /^vct_[A-Za-z0-9_-]{43}$/;
|
|
13
20
|
const RELEASE_TIMEOUT = Symbol("release-timeout");
|
|
14
21
|
const RESPONSE_ACTIVITY = Symbol("response-activity");
|
|
15
22
|
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
16
23
|
const MAX_FRAME_BYTES = 1024 * 1024;
|
|
17
24
|
const MAX_EVENTS = 10_000;
|
|
25
|
+
const CAS_REF_SHAPE = /^sha256:([0-9a-f]{64})$/;
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
export function redact(value) {
|
|
@@ -28,57 +36,128 @@ function refuseSecretValue(value) {
|
|
|
28
36
|
return value;
|
|
29
37
|
}
|
|
30
38
|
|
|
39
|
+
function canonicalOrigin(raw) {
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new CliError("origin_invalid", "origin must be an absolute public origin");
|
|
45
|
+
}
|
|
46
|
+
const lexicalLoopback = /^http:\/\/(?:127\.0\.0\.1|localhost|\[::1\])(?::\d{1,5})?$/.test(raw);
|
|
47
|
+
if ((url.protocol !== "https:" && !lexicalLoopback) ||
|
|
48
|
+
url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
49
|
+
throw new CliError("origin_invalid", "origin must be HTTPS or an exact loopback HTTP origin");
|
|
50
|
+
}
|
|
51
|
+
return url.origin;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function originKey(raw) {
|
|
55
|
+
const stripped = String(raw).replace(/\/+$/, "");
|
|
56
|
+
try { return new URL(stripped).origin; } catch { return stripped; }
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
/**
|
|
32
|
-
* Where a run gets its origin and its token, in one place
|
|
60
|
+
* Where a run gets its origin and its token, in one place.
|
|
33
61
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
62
|
+
* Origin precedence is `--origin` > `VECTEUR_BASE_URL` > the store's current origin. The
|
|
63
|
+
* environment token still wins, because that is how CI and a container pass a credential
|
|
64
|
+
* and neither should have to write a file to do it. `VECTEUR_TOKEN` is the CI path and is
|
|
65
|
+
* never written or cleared by login or logout.
|
|
38
66
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
67
|
+
* A stored login does not lend its token to a different origin named by `--origin` or
|
|
68
|
+
* `VECTEUR_BASE_URL`: a token minted for one deployment silently pointed at another is a
|
|
69
|
+
* credential leak with a good explanation.
|
|
42
70
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The one origin rule: `--origin` > `VECTEUR_BASE_URL` > the store's current origin > the
|
|
73
|
+
* public product. `login` resolved it separately and reached a different answer on an empty
|
|
74
|
+
* machine — it fell back to the product, `configuration` fell back to the empty string — which
|
|
75
|
+
* is what two owners of one rule always eventually do.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveOrigin(env = process.env, stored = undefined, originFlag = undefined) {
|
|
78
|
+
const saved = stored === undefined && !env.VECTEUR_TOKEN ? readCredentials(env) : stored;
|
|
79
|
+
return canonicalOrigin(
|
|
80
|
+
originFlag ?? env.VECTEUR_BASE_URL ?? saved?.current ?? DEFAULT_ORIGIN);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The one timeout rule, so a bound the client enforces is not re-stated by its caller. */
|
|
84
|
+
export function requestTimeout(env = process.env) {
|
|
85
|
+
const timeout = Number(env.VECTEUR_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS);
|
|
86
|
+
if (!Number.isInteger(timeout) || timeout < 100 || timeout > 120_000) {
|
|
87
|
+
throw new CliError("timeout_invalid", "VECTEUR_TIMEOUT_MS must be 100 to 120000");
|
|
88
|
+
}
|
|
89
|
+
return timeout;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The one token-shape rule, so `login` and `configuration` cannot disagree about a credential. */
|
|
93
|
+
export function isTokenShaped(value) {
|
|
94
|
+
return TOKEN_SHAPE.test(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function configuration(env = process.env, stored = undefined, originFlag = undefined) {
|
|
98
|
+
// THE CI PATH DOES NOT READ THE STORE. `VECTEUR_TOKEN` is how a container and a CI job hand
|
|
99
|
+
// over a credential, and neither has a store; reading one anyway meant an unreadable file on
|
|
100
|
+
// the machine broke a run that never needed it — `credentials_unreadable` for a credential
|
|
101
|
+
// the environment had already supplied.
|
|
102
|
+
const saved = env.VECTEUR_TOKEN
|
|
103
|
+
? null
|
|
104
|
+
: (stored === undefined ? readCredentials(env) : stored);
|
|
105
|
+
const named = originFlag ?? env.VECTEUR_BASE_URL;
|
|
46
106
|
|
|
47
107
|
// A STORED TOKEN GOES TO THE ORIGIN IT WAS STORED FOR, AND NOWHERE ELSE.
|
|
48
|
-
|
|
49
|
-
// The two sources resolve field by field, which on its own lets `VECTEUR_BASE_URL=<other>`
|
|
50
|
-
// borrow the stored token and send it somewhere it was never minted for — a credential leak
|
|
51
|
-
// with a plausible explanation, and one a user would never see happen. If the environment
|
|
52
|
-
// names an origin, it must bring its own token.
|
|
53
|
-
if (!env.VECTEUR_TOKEN && saved && env.VECTEUR_BASE_URL &&
|
|
54
|
-
env.VECTEUR_BASE_URL.replace(/\/+$/, "") !== saved.origin) {
|
|
108
|
+
if (!env.VECTEUR_TOKEN && saved && named && !Object.hasOwn(saved.logins, originKey(named))) {
|
|
55
109
|
throw new CliError("origin_mismatch",
|
|
56
|
-
`the stored login is for ${saved.
|
|
57
|
-
+ `${
|
|
110
|
+
`the stored login is for ${saved.current} and ${originFlag !== undefined ? "--origin" : "VECTEUR_BASE_URL"} names `
|
|
111
|
+
+ `${named} — set VECTEUR_TOKEN for that origin, or log in to it`);
|
|
58
112
|
}
|
|
59
113
|
|
|
60
|
-
|
|
114
|
+
const origin = resolveOrigin(env, saved, originFlag);
|
|
115
|
+
const token = env.VECTEUR_TOKEN ?? saved?.logins?.[originKey(origin)]?.token ?? "";
|
|
116
|
+
|
|
117
|
+
if (!isTokenShaped(token)) {
|
|
61
118
|
throw new CliError("token_invalid", saved
|
|
62
119
|
? "the stored login does not hold one personal access token — run `vecteur login` again"
|
|
63
|
-
: "no login found: run `vecteur login
|
|
120
|
+
: "no login found: run `vecteur login`, or set VECTEUR_TOKEN");
|
|
64
121
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
122
|
+
return { origin, token, timeout: requestTimeout(env),
|
|
123
|
+
projectId: saved?.logins?.[originKey(origin)]?.project_id };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function shouldOpenBrowser(noBrowser, env = process.env) {
|
|
127
|
+
if (noBrowser) return false;
|
|
128
|
+
if (process.platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return false;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function delay(ms) {
|
|
133
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function waitForDeviceAuthorization(client, grant, { sleep = delay, now = Date.now } = {}) {
|
|
137
|
+
if (!validateDeviceAuthorization(grant)) {
|
|
138
|
+
throw new CliError("device_authorization_invalid", "Server returned malformed owner data");
|
|
76
139
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
140
|
+
const intervalMs = grant.interval * 1000;
|
|
141
|
+
while (true) {
|
|
142
|
+
if (now() >= grant.expires_at * 1000) {
|
|
143
|
+
throw new CliError("device_expired", "This code has expired");
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const polled = await client.devicePoll(grant.device_code);
|
|
147
|
+
if (validatePatCreated(polled)) return polled;
|
|
148
|
+
if (polled && polled.status === "authorization_pending") {
|
|
149
|
+
await sleep(intervalMs);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
throw new CliError("device_poll_invalid", "Server returned malformed owner data");
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error instanceof CliError && error.code === "device_slow_down") {
|
|
155
|
+
await sleep(intervalMs);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
80
160
|
}
|
|
81
|
-
return { origin: url.origin, token, timeout };
|
|
82
161
|
}
|
|
83
162
|
|
|
84
163
|
function exactObject(value, fields, code) {
|
|
@@ -231,7 +310,8 @@ export class VecteurClient {
|
|
|
231
310
|
redirect: "manual",
|
|
232
311
|
signal: controller.signal,
|
|
233
312
|
headers: {
|
|
234
|
-
|
|
313
|
+
"X-Vecteur-Client": OWNER_CONTRACT_IDENTITY,
|
|
314
|
+
...(this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {}),
|
|
235
315
|
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
236
316
|
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
|
|
237
317
|
...(accept ? { Accept: accept } : {}),
|
|
@@ -247,20 +327,84 @@ export class VecteurClient {
|
|
|
247
327
|
signal?.removeEventListener("abort", cancel);
|
|
248
328
|
clearTimeout(overallTimer); clearTimeout(idleTimer);
|
|
249
329
|
};
|
|
330
|
+
if (response.headers.get("X-Vecteur-Contract") === null) {
|
|
331
|
+
throw new CliError(
|
|
332
|
+
"origin_not_vecteur",
|
|
333
|
+
`origin ${this.config.origin}; cause=missing_vecteur_contract; next: pass the Vecteur origin with --origin`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
250
336
|
if (response.status >= 300 && response.status < 400) {
|
|
251
337
|
throw new CliError("redirect_refused", "Public API redirects are refused");
|
|
252
338
|
}
|
|
253
339
|
if (!response.ok) {
|
|
254
340
|
let code = `http_${response.status}`;
|
|
341
|
+
let ownerMessage = "";
|
|
342
|
+
let refused = null;
|
|
255
343
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
344
|
+
// THE GUARD RUNS ON REFUSALS TOO. It sat on the success path alone, so a 401 whose
|
|
345
|
+
// body carried `vct_…` was read for its `error.code` and reported as `pat_revoked`:
|
|
346
|
+
// the one exception to the guard is an HTTP 201 from the device poll, and an error
|
|
347
|
+
// is not it. A credential in a refusal is still a credential the door leaked.
|
|
348
|
+
const body = refuseSecretValue(parseOwnerJson(await boundedText(response)));
|
|
349
|
+
const error = body.error;
|
|
350
|
+
if (error && typeof error === "object" && !Array.isArray(error)) {
|
|
351
|
+
const candidate = error.code;
|
|
352
|
+
if (typeof candidate === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(candidate)) {
|
|
353
|
+
code = candidate;
|
|
354
|
+
}
|
|
355
|
+
if (candidate === "project_quota_exhausted") {
|
|
356
|
+
if (!validateQuotaDenial(error.quota) || typeof error.message !== "string"
|
|
357
|
+
|| !error.message.length || /[\r\n\0]/.test(error.message)) {
|
|
358
|
+
throw new CliError("quota_invalid", "Server returned malformed Project quota data");
|
|
359
|
+
}
|
|
360
|
+
const actions = error.quota.recovery.map(id => {
|
|
361
|
+
if (!Object.hasOwn(RECOVERY_ACTIONS, id) || !RECOVERY_ACTIONS[id].href) {
|
|
362
|
+
throw new CliError("recovery_action_invalid", `Unknown Project recovery action: ${id}`);
|
|
363
|
+
}
|
|
364
|
+
const action = RECOVERY_ACTIONS[id];
|
|
365
|
+
return `${action.label}: ${new URL(action.href, this.config.origin).href}`;
|
|
366
|
+
});
|
|
367
|
+
const quota = error.quota;
|
|
368
|
+
ownerMessage = `${error.message}; ${quota.current} of ${quota.maximum} active projects used on the ${quota.tier} plan`
|
|
369
|
+
+ (actions.length ? `; next: ${actions.join("; ")}` : "");
|
|
370
|
+
}
|
|
371
|
+
if (candidate === "customer_cost_exhausted") {
|
|
372
|
+
if (!validateLimitDenial(error.limit)) {
|
|
373
|
+
throw new CliError("customer_cost_invalid", "Server returned malformed customer-cost refusal");
|
|
374
|
+
}
|
|
375
|
+
const limit = error.limit;
|
|
376
|
+
const binding = `${limit.binding_scope.kind}:${limit.binding_scope.scope.type}:${limit.binding_scope.scope.id}`;
|
|
377
|
+
const reset = new Date(limit.reset_at * 1000).toISOString().replace(".000Z", "Z");
|
|
378
|
+
ownerMessage = `Customer cost limit reached; binding=${binding}; reset=${reset} UTC; next=${limit.recovery.join(",")}`;
|
|
379
|
+
}
|
|
380
|
+
if (["pat_expired", "pat_invalid", "pat_revoked"].includes(candidate)) {
|
|
381
|
+
ownerMessage = "credential is expired or revoked; next: vecteur login";
|
|
382
|
+
}
|
|
383
|
+
if (candidate === "pat_ancestry_inactive") {
|
|
384
|
+
ownerMessage = "seat or Organization is inactive; next: contact your Organization owner or Vecteur support";
|
|
385
|
+
}
|
|
386
|
+
if (
|
|
387
|
+
candidate === "client_outdated"
|
|
388
|
+
&& Object.keys(error).sort().join("\n") === "code\nmessage"
|
|
389
|
+
&& typeof error.message === "string"
|
|
390
|
+
&& error.message.length > 0
|
|
391
|
+
&& error.message.length <= 512
|
|
392
|
+
&& !/[\r\n\0]/.test(error.message)
|
|
393
|
+
) {
|
|
394
|
+
try { ownerMessage = refuseSecretValue(error.message); } catch {}
|
|
395
|
+
}
|
|
259
396
|
}
|
|
260
397
|
} catch (error) {
|
|
261
398
|
if (error?.name === "AbortError") throw error;
|
|
399
|
+
if (error instanceof CliError && [
|
|
400
|
+
"secret_response_refused", "quota_invalid", "recovery_action_invalid", "customer_cost_invalid",
|
|
401
|
+
].includes(error.code)) refused = error;
|
|
262
402
|
}
|
|
263
|
-
|
|
403
|
+
if (refused) throw refused;
|
|
404
|
+
throw new CliError(
|
|
405
|
+
code,
|
|
406
|
+
ownerMessage || `Public API refused request with HTTP ${response.status}`,
|
|
407
|
+
);
|
|
264
408
|
}
|
|
265
409
|
return response;
|
|
266
410
|
} catch (error) {
|
|
@@ -268,13 +412,33 @@ export class VecteurClient {
|
|
|
268
412
|
clearTimeout(overallTimer); clearTimeout(idleTimer);
|
|
269
413
|
if (error?.name === "AbortError") {
|
|
270
414
|
if (signal?.aborted) throw new CliError("request_cancelled", "Public API request was cancelled");
|
|
271
|
-
throw new CliError("request_timeout",
|
|
415
|
+
throw new CliError("request_timeout",
|
|
416
|
+
`origin ${this.config.origin}; cause=timeout; next: check the network and retry`);
|
|
272
417
|
}
|
|
273
418
|
if (error instanceof CliError) throw error;
|
|
274
|
-
throw new CliError("request_failed",
|
|
419
|
+
throw new CliError("request_failed",
|
|
420
|
+
`origin ${this.config.origin}; cause=transport; next: check the network and --origin`);
|
|
275
421
|
}
|
|
276
422
|
}
|
|
277
423
|
|
|
424
|
+
async projectList(signal = undefined) {
|
|
425
|
+
const value = await this.#json("GET", "/api/projects", { signal });
|
|
426
|
+
if (!Array.isArray(value) || !value.every(validateProject)) {
|
|
427
|
+
throw new CliError("projects_invalid", "Server returned malformed Project list");
|
|
428
|
+
}
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async projectCreate(name, workspaceId, signal = undefined) {
|
|
433
|
+
const value = await this.#json("POST", "/api/projects", {
|
|
434
|
+
body: { name, workspace_id: workspaceId }, signal,
|
|
435
|
+
});
|
|
436
|
+
if (!validateProject(value) || value.workspace_id !== workspaceId) {
|
|
437
|
+
throw new CliError("project_invalid", "Server returned malformed project data");
|
|
438
|
+
}
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
|
|
278
442
|
async projectGet(projectId, signal = undefined) {
|
|
279
443
|
const value = await this.#json("GET", `/api/projects/${encodeURIComponent(projectId)}`, { signal });
|
|
280
444
|
if (!validateProject(value) || value.id !== projectId) {
|
|
@@ -283,6 +447,86 @@ export class VecteurClient {
|
|
|
283
447
|
return value;
|
|
284
448
|
}
|
|
285
449
|
|
|
450
|
+
async deviceMint(signal = undefined) {
|
|
451
|
+
const value = await this.#json("POST", "/api/auth/device", { body: {}, signal });
|
|
452
|
+
if (!validateDeviceAuthorization(value)) {
|
|
453
|
+
throw new CliError("device_authorization_invalid", "Server returned malformed owner data");
|
|
454
|
+
}
|
|
455
|
+
return value;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async devicePoll(deviceCode, signal = undefined) {
|
|
459
|
+
return this.#json("POST", "/api/auth/device/poll", { body: { device_code: deviceCode }, signal });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async accountMe(signal = undefined) {
|
|
463
|
+
const value = await this.#json("GET", "/api/auth/me", { signal });
|
|
464
|
+
if (!validateAccount(value)) throw new CliError("account_invalid", "Server returned malformed owner data");
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async credentialSelfRevoke(signal = undefined) {
|
|
469
|
+
return this.#json("DELETE", "/api/credentials/self", { signal });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async customerCost(projectId, signal = undefined) {
|
|
473
|
+
const query = new URLSearchParams({ scope: "project", scope_id: projectId, period: "utc_month" });
|
|
474
|
+
const value = await this.#json("GET", `/api/customer-cost?${query}`, { signal });
|
|
475
|
+
if (!validateCustomerCostProjection(value) || value.scope.type !== "project"
|
|
476
|
+
|| value.scope.id !== projectId) {
|
|
477
|
+
throw new CliError("customer_cost_invalid", "Server returned malformed customer-cost projection");
|
|
478
|
+
}
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async artifactGet(casRef, projectId, onChunk, signal = undefined) {
|
|
483
|
+
const identity = CAS_REF_SHAPE.exec(casRef);
|
|
484
|
+
if (!identity || typeof projectId !== "string" || !projectId
|
|
485
|
+
|| typeof onChunk !== "function") {
|
|
486
|
+
throw new CliError("artifact_invalid", "artifact get requires an admitted sha256 CAS id and Project");
|
|
487
|
+
}
|
|
488
|
+
const query = new URLSearchParams({ project_id: projectId });
|
|
489
|
+
const response = await this.request(
|
|
490
|
+
"GET", `/api/artifacts/${encodeURIComponent(casRef)}?${query}`, { signal },
|
|
491
|
+
);
|
|
492
|
+
try {
|
|
493
|
+
const contentType = response.headers.get("content-type");
|
|
494
|
+
if (!Object.values(ARTIFACT_MEDIA_TYPES).includes(contentType)) {
|
|
495
|
+
throw new CliError("artifact_invalid", "Server returned an undeclared artifact media type");
|
|
496
|
+
}
|
|
497
|
+
const declared = response.headers.get("content-length");
|
|
498
|
+
if (declared === null || !/^(?:0|[1-9][0-9]*)$/.test(declared)
|
|
499
|
+
|| !Number.isSafeInteger(Number(declared))) {
|
|
500
|
+
throw new CliError("artifact_invalid", "Server returned an invalid artifact length");
|
|
501
|
+
}
|
|
502
|
+
if (!response.body) throw new CliError("artifact_invalid", "Server returned no artifact bytes");
|
|
503
|
+
const digest = createHash("sha256");
|
|
504
|
+
let bytes = 0;
|
|
505
|
+
for await (const chunk of response.body) {
|
|
506
|
+
response[RESPONSE_ACTIVITY]?.();
|
|
507
|
+
bytes += chunk.byteLength;
|
|
508
|
+
if (bytes > Number(declared)) {
|
|
509
|
+
throw new CliError("artifact_invalid", "Artifact bytes exceed the declared length");
|
|
510
|
+
}
|
|
511
|
+
digest.update(chunk);
|
|
512
|
+
await onChunk(chunk);
|
|
513
|
+
}
|
|
514
|
+
if (bytes !== Number(declared)) {
|
|
515
|
+
throw new CliError("artifact_invalid", "Artifact bytes differ from the declared length");
|
|
516
|
+
}
|
|
517
|
+
if (digest.digest("hex") !== identity[1]) {
|
|
518
|
+
throw new CliError("artifact_invalid", "Artifact bytes differ from the admitted digest");
|
|
519
|
+
}
|
|
520
|
+
return { cas_ref: casRef, project_id: projectId, content_type: contentType, bytes };
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (error instanceof CliError) throw error;
|
|
523
|
+
throw new CliError("request_failed",
|
|
524
|
+
`origin ${this.config.origin}; cause=artifact_transport; next: check the network and retry`);
|
|
525
|
+
} finally {
|
|
526
|
+
response[RELEASE_TIMEOUT]?.();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
286
530
|
async runSubmit(projectId, ask, onEvent = null, signal = undefined) {
|
|
287
531
|
const value = await this.#json("POST", `/api/projects/${encodeURIComponent(projectId)}/runs`, {
|
|
288
532
|
body: { ask, attachments: [], parent_state_hash: null, ui_context: null },
|
|
@@ -292,7 +536,15 @@ export class VecteurClient {
|
|
|
292
536
|
if (!validateRunAck(value)) {
|
|
293
537
|
throw new CliError("run_ack_invalid", "Server returned malformed run acknowledgement");
|
|
294
538
|
}
|
|
295
|
-
|
|
539
|
+
try {
|
|
540
|
+
return await this.runEvents(value.run_id, onEvent, signal);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (error instanceof CliError && error.code === "pat_expired") {
|
|
543
|
+
throw new CliError("pat_expired",
|
|
544
|
+
`credential expired after Run ${value.run_id} was admitted; next: vecteur login; then: vecteur run get ${value.run_id}`);
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
296
548
|
}
|
|
297
549
|
|
|
298
550
|
async runGet(runId, signal = undefined) {
|
|
@@ -318,7 +570,8 @@ export class VecteurClient {
|
|
|
318
570
|
events = await parseSse(response, runId, onEvent);
|
|
319
571
|
} catch (error) {
|
|
320
572
|
if (error?.name === "AbortError") {
|
|
321
|
-
throw new CliError("request_timeout",
|
|
573
|
+
throw new CliError("request_timeout",
|
|
574
|
+
`origin ${this.config.origin}; cause=timeout; next: check the network and retry`);
|
|
322
575
|
}
|
|
323
576
|
throw error;
|
|
324
577
|
} finally {
|
|
@@ -344,7 +597,19 @@ export class VecteurClient {
|
|
|
344
597
|
if (response.headers.get("content-type")?.split(";", 1)[0] !== "application/json") {
|
|
345
598
|
throw new CliError("response_invalid", "Public API response is not JSON");
|
|
346
599
|
}
|
|
347
|
-
try {
|
|
600
|
+
try {
|
|
601
|
+
const value = parseOwnerJson(await boundedText(response));
|
|
602
|
+
// Sole exception to refuseSecretValue: HTTP 201 from POST /api/auth/device/poll,
|
|
603
|
+
// accepted only after exact validation of the owner PatCreated shape. Pending
|
|
604
|
+
// polls, every other status and every other route keep the guard.
|
|
605
|
+
if (method === "POST" && path === "/api/auth/device/poll" && response.status === 201) {
|
|
606
|
+
if (!validatePatCreated(value)) {
|
|
607
|
+
throw new CliError("pat_created_invalid", "Server returned malformed owner data");
|
|
608
|
+
}
|
|
609
|
+
return value;
|
|
610
|
+
}
|
|
611
|
+
return refuseSecretValue(value);
|
|
612
|
+
}
|
|
348
613
|
catch (error) {
|
|
349
614
|
if (error?.name === "AbortError") {
|
|
350
615
|
throw new CliError("request_timeout", "Public API request timed out");
|
package/src/contract.js
CHANGED
|
@@ -1,12 +1,47 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-contract.mjs from owner TypeScript projections.
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
6
|
-
const
|
|
2
|
+
export const OWNER_CONTRACT_IDENTITY = "api-sha256:f51b0fce34f18d782dbc403d177cab30b03ad4506c8453d8aaa106af476053a9;run-wire-sha256:3a9467e54836696b554ffa163decf62704323ea8943f57857c6d719cf8d88674";
|
|
3
|
+
export const RECOVERY_ACTIONS = Object.freeze({"delete_project":{"label":"Delete a project","href":"/projects"},"upgrade_plan":{"label":"Upgrade your plan","href":"/dashboard/billing"},"wait_for_reset":{"label":"Wait until the UTC reset","href":null}});
|
|
4
|
+
export const EVENT_KINDS = Object.freeze(["source","step_started","step_finished","synthesis","artifact","cta","state_advanced","terminal","heartbeat","assistant_delta","step_label","context_frame","knowledge_binding","engineering_projection","provider_attempt","clarification"]);
|
|
5
|
+
export const TERMINAL_FIELDS = Object.freeze(["answer","defect","evidence_dag","instruction_source","intent","missing_input_need","missing_inputs","provider_retry_offer","recovery","state","steps"]);
|
|
6
|
+
export const ARTIFACT_MEDIA_TYPES = Object.freeze({"code":"text/plain","csv":"text/csv","docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","drive":"application/vnd.google-drive","html":"text/html","md":"text/markdown","pdf":"application/pdf","xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});
|
|
7
|
+
export const TERMINAL_RECOVERY_DECLARATIONS = Object.freeze({"artifact_publication_missing":[["start_run",["project_id"]]],"artifact_store_unavailable":[["start_run",["project_id"]]],"budget_exhausted":[["start_run",["project_id"]]],"cancelled":[["start_run",["project_id"]]],"cause_unrecorded":[["start_run",["project_id"]]],"clarification_custody_invalid":[["start_run",["project_id"]]],"clarification_custody_lost":[["start_run",["project_id"]]],"clarification_resume_invalid":[["start_run",["project_id"]]],"clarification_unpersisted":[["start_run",["project_id"]]],"context_unselected":[["start_run",["project_id"]]],"credential_class_disclosure":[["start_run",["project_id"]]],"design_state_missing":[["start_run",["project_id"]]],"evidence_admission_invalid":[["start_run",["project_id"]]],"evidence_artifact_invalid":[["start_run",["project_id"]]],"evidence_artifact_unbound":[["start_run",["project_id"]]],"evidence_binding_unresolved":[["start_run",["project_id"]]],"evidence_context_invalid":[["start_run",["project_id"]]],"evidence_extract_unreached":[["start_run",["project_id"]]],"evidence_missing":[["start_run",["project_id"]]],"evidence_owner_receipt_mismatch":[["start_run",["project_id"]]],"evidence_owner_receipt_missing":[["start_run",["project_id"]]],"evidence_payload_invalid":[["start_run",["project_id"]]],"evidence_reached_invalid":[["start_run",["project_id"]]],"evidence_replay_invalid":[["start_run",["project_id"]]],"evidence_run_mismatch":[["start_run",["project_id"]]],"evidence_seal_collision":[["start_run",["project_id"]]],"evidence_selected_unreached":[["start_run",["project_id"]]],"execution_route_invalid":[["start_run",["project_id"]]],"executor_root_invalid":[["start_run",["project_id"]]],"extraction_offer_invalid":[["start_run",["project_id"]]],"extraction_offer_tampered":[["start_run",["project_id"]]],"fact_support_unread":[["start_run",["project_id"]]],"harness_arm_invalid":[["start_run",["project_id"]]],"harness_event_unmapped":[["start_run",["project_id"]]],"harness_exited":[["start_run",["project_id"]]],"harness_mapping_failed":[["start_run",["project_id"]]],"harness_session_invalid":[["start_run",["project_id"]]],"harness_session_missing":[["start_run",["project_id"]]],"harness_timeout":[["start_run",["project_id"]]],"lease_interval_invalid":[["start_run",["project_id"]]],"ledger_malformed":[["start_run",["project_id"]]],"max_tokens":[["start_run",["project_id"]]],"profile_eligibility_changed":[["start_run",["project_id"]]],"prompt_provenance_unavailable":[["start_run",["project_id"]]],"provider_budget_exhausted":[["start_run",["project_id"]]],"provider_budget_missing":[["start_run",["project_id"]]],"provider_budget_unavailable":[["start_run",["project_id"]]],"provider_relay_absent":[["start_run",["project_id"]]],"provider_route_control_invalid":[["start_run",["project_id"]]],"provider_route_exhausted":[["start_run",["project_id"]],["retry_run",["source_run_id","offer_id","generation"]]],"provider_route_failure_unobserved":[["start_run",["project_id"]]],"provider_route_reconciliation_invalid":[["start_run",["project_id"]]],"provider_route_restart_terminal":[["start_run",["project_id"]]],"provider_training_opted_in":[["start_run",["project_id"]]],"publish_seal_missing":[["start_run",["project_id"]]],"replay_capture_digest_mismatch":[["start_run",["project_id"]]],"replay_capture_invalid":[["start_run",["project_id"]]],"replay_config_incomplete":[["start_run",["project_id"]]],"replay_failed":[["start_run",["project_id"]]],"replay_no_requests":[["start_run",["project_id"]]],"replay_patch_invalid":[["start_run",["project_id"]]],"replay_plugin_absent":[["start_run",["project_id"]]],"replay_provider_socket":[["start_run",["project_id"]]],"replay_receipt_invalid":[["start_run",["project_id"]]],"replay_receipt_owner_chown":[["start_run",["project_id"]]],"replay_receipt_owner_incomplete":[["start_run",["project_id"]]],"replay_receipt_owner_invalid":[["start_run",["project_id"]]],"replay_request_observation_count":[["start_run",["project_id"]]],"replay_request_observation_invalid":[["start_run",["project_id"]]],"replay_request_observation_missing":[["start_run",["project_id"]]],"replay_request_owner_chown":[["start_run",["project_id"]]],"replay_socket_observation_invalid":[["start_run",["project_id"]]],"replay_socket_observation_missing":[["start_run",["project_id"]]],"replay_socket_owner_chown":[["start_run",["project_id"]]],"run_database_capability_refused":[["start_run",["project_id"]]],"runtime_unavailable":[["start_run",["project_id"]]],"sealed_contract_violated":[["start_run",["project_id"]]],"sealed_file_missing":[["start_run",["project_id"]]],"seat_patch_absent":[["start_run",["project_id"]]],"seat_patch_drift":[["start_run",["project_id"]]],"selection_admission_invalid":[["start_run",["project_id"]]],"selection_context_invalid":[["start_run",["project_id"]]],"selection_receipt_conflict":[["start_run",["project_id"]]],"selection_receipt_invalid":[["start_run",["project_id"]]],"selection_receipt_missing":[["start_run",["project_id"]]],"selection_required":[["start_run",["project_id"]]],"state_corrupt":[["start_run",["project_id"]]],"state_file_invalid":[["start_run",["project_id"]]],"state_invalid":[["start_run",["project_id"]]],"state_payload_overflow":[["start_run",["project_id"]]],"state_unencodable":[["start_run",["project_id"]]],"state_version_unknown":[["start_run",["project_id"]]],"usage_egress_mismatch":[["start_run",["project_id"]]]});
|
|
8
|
+
export const ORIGIN_KINDS = Object.freeze(["user","hypothesis","derived","knowledge","file","catalog","default"]);
|
|
9
|
+
export const ORIGIN_WORD = Object.freeze({"user":"your brief","hypothesis":"assumed","derived":"derived","knowledge":"Vecteur knowledge","file":"your file","catalog":"catalogue","default":"default","unattributed":"unstated"});
|
|
10
|
+
const shape=(v,r,o=[])=>!!v&&typeof v==="object"&&!Array.isArray(v)&&r.every(k=>Object.hasOwn(v,k))&&Object.keys(v).every(k=>r.includes(k)||o.includes(k));
|
|
11
|
+
const exact=(v,k)=>shape(v,k)&&Object.keys(v).length===k.length;
|
|
7
12
|
const text=(v)=>typeof v==="string"&&v.length>0; const integer=(v)=>Number.isSafeInteger(v);
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
+
const ownerAccount=v=>shape(v,["id","email","display_name","tier","org_id","organization_role","workspaces","connectors","capabilities","entitlements","avatar_url","display_currency","model_cost_estimates","is_platform_operator"],[])&&(typeof v.id==="string")&&(typeof v.email==="string")&&(typeof v.display_name==="string")&&(ownerTier(v.tier))&&(typeof v.org_id==="string")&&((v.organization_role==="OWNER"||v.organization_role==="ADMIN"||v.organization_role==="MEMBER"))&&(Array.isArray(v.workspaces)&&v.workspaces.every(x=>ownerWorkspace(x)))&&(Array.isArray(v.connectors)&&v.connectors.every(x=>typeof x==="string"))&&(Array.isArray(v.capabilities)&&v.capabilities.every(x=>ownerCapability(x)))&&(ownerEntitlements(v.entitlements))&&((typeof v.avatar_url==="string"||v.avatar_url===null))&&((v.display_currency==="EUR"||v.display_currency==="USD"))&&(ownerModelCostEstimates(v.model_cost_estimates))&&(typeof v.is_platform_operator==="boolean");
|
|
14
|
+
const ownerTier=v=>(v==="free"||v==="pro"||v==="enterprise");
|
|
15
|
+
const ownerWorkspace=v=>shape(v,["id","org_id","name","status"],[])&&(typeof v.id==="string")&&(typeof v.org_id==="string")&&(typeof v.name==="string")&&(v.status==="active");
|
|
16
|
+
const ownerCapability=v=>shape(v,["id","affordance","state","required_tier","reason","recovery"],[])&&(typeof v.id==="string")&&((v.affordance==="composer-source"||v.affordance==="workspace-export"||v.affordance==="composer-preference"))&&((v.state==="available"||v.state==="locked"||v.state==="disconnected"||v.state==="unavailable"))&&((v.required_tier==="pro"||v.required_tier==="enterprise"||v.required_tier===null))&&(typeof v.reason==="string")&&((typeof v.recovery==="string"||v.recovery===null));
|
|
17
|
+
const ownerEntitlements=v=>shape(v,["active_projects","seats"],[])&&(ownerActiveProjectEntitlement(v.active_projects))&&(ownerSeatEntitlement(v.seats));
|
|
18
|
+
const ownerActiveProjectEntitlement=v=>shape(v,["resource","current","maximum","state","reason","recovery"],[])&&(v.resource==="active_projects")&&(Number.isFinite(v.current))&&((Number.isFinite(v.maximum)||v.maximum===null))&&((v.state==="available"||v.state==="locked"))&&(typeof v.reason==="string")&&(Array.isArray(v.recovery)&&v.recovery.every(x=>(x==="delete_project"||x==="upgrade_plan")));
|
|
19
|
+
const ownerSeatEntitlement=v=>shape(v,["current","states","reason"],[])&&(Number.isFinite(v.current))&&(v.states!==null&&typeof v.states==="object"&&!Array.isArray(v.states)&&Object.values(v.states).every(x=>Number.isFinite(x)))&&(typeof v.reason==="string");
|
|
20
|
+
const ownerModelCostEstimates=v=>shape(v,["schema","catalogue_envelope_sha256","reference_sha256","projection_sha256","workload","display_currency","rows","cost_ticks"],[])&&(v.schema==="vecteur.model-cost-estimates.v4")&&(typeof v.catalogue_envelope_sha256==="string")&&(typeof v.reference_sha256==="string")&&(typeof v.projection_sha256==="string")&&(typeof v.workload==="string")&&((v.display_currency==="EUR"||v.display_currency==="USD"))&&(Array.isArray(v.rows)&&v.rows.every(x=>ownerModelCostEstimate(x)))&&(Array.isArray(v.cost_ticks)&&v.cost_ticks.every(x=>ownerModelCostTick(x)));
|
|
21
|
+
const ownerModelCostEstimate=v=>shape(v,["selection_id","customer_gross_microusd","display"],[])&&(typeof v.selection_id==="string")&&(Number.isFinite(v.customer_gross_microusd))&&(typeof v.display==="string");
|
|
22
|
+
const ownerModelCostTick=v=>shape(v,["customer_gross_microusd","display"],[])&&(Number.isFinite(v.customer_gross_microusd))&&(typeof v.display==="string");
|
|
23
|
+
const ownerQuotaDenial=v=>shape(v,["resource","current","maximum","tier","recovery"],[])&&(typeof v.resource==="string")&&(Number.isFinite(v.current))&&(Number.isFinite(v.maximum))&&(typeof v.tier==="string")&&(Array.isArray(v.recovery)&&v.recovery.every(x=>typeof x==="string"));
|
|
24
|
+
const ownerCustomerCostProjection=v=>shape(v,["schema","scope","binding_scope","period_start","period_end","unit","used","limit","remaining","reset_at","coverage","display"],[])&&(v.schema==="vecteur.customer-cost-projection.v1")&&(ownerCostScope(v.scope))&&(ownerCostBindingScope(v.binding_scope))&&(Number.isFinite(v.period_start))&&(Number.isFinite(v.period_end))&&(v.unit==="customer_gross_microusd")&&((Number.isFinite(v.used)||v.used===null))&&((Number.isFinite(v.limit)||v.limit===null))&&((Number.isFinite(v.remaining)||v.remaining===null))&&(Number.isFinite(v.reset_at))&&((v.coverage==="complete"||v.coverage==="incomplete"))&&(ownerCustomerCostDisplay(v.display));
|
|
25
|
+
const ownerCostScope=v=>shape(v,["type","id"],[])&&((v.type==="organization"||v.type==="workspace"||v.type==="project"||v.type==="user"))&&(typeof v.id==="string");
|
|
26
|
+
const ownerCostBindingScope=v=>shape(v,["kind","scope"],[])&&((v.kind==="platform_tier"||v.kind==="restriction"||v.kind==="unlimited"))&&(ownerCostScope(v.scope));
|
|
27
|
+
const ownerCustomerCostDisplay=v=>shape(v,["currency","used","limit","remaining"],[])&&((v.currency==="EUR"||v.currency==="USD"))&&((typeof v.used==="string"||v.used===null))&&((typeof v.limit==="string"||v.limit===null))&&((typeof v.remaining==="string"||v.remaining===null));
|
|
28
|
+
const ownerLimitDenial=v=>shape(v,["schema","scope","binding_scope","period_start","period_end","unit","used","limit","remaining","reset_at","coverage","display","dimension","recovery"],[])&&(v.schema==="vecteur.customer-cost-projection.v1")&&(ownerCostScope(v.scope))&&(ownerCostBindingScope(v.binding_scope))&&(Number.isFinite(v.period_start))&&(Number.isFinite(v.period_end))&&(v.unit==="customer_gross_microusd")&&((Number.isFinite(v.used)||v.used===null))&&((Number.isFinite(v.limit)||v.limit===null))&&((Number.isFinite(v.remaining)||v.remaining===null))&&(Number.isFinite(v.reset_at))&&((v.coverage==="complete"||v.coverage==="incomplete"))&&(ownerCustomerCostDisplay(v.display))&&(v.dimension==="customer_cost")&&(Array.isArray(v.recovery)&&v.recovery.every(x=>(x==="wait_for_reset"||x==="upgrade_plan")));
|
|
29
|
+
const ownerArtifact=v=>shape(v,["id","kind","cas_ref","filename"],["produced_by_step","from_input_rows","build_receipt_id","output_index"])&&(typeof v.id==="string")&&((v.kind==="docx"||v.kind==="xlsx"||v.kind==="pdf"||v.kind==="md"||v.kind==="csv"||v.kind==="html"||v.kind==="code"||v.kind==="drive"))&&(ownerCasRef(v.cas_ref))&&(typeof v.filename==="string")&&(v.produced_by_step===undefined||(typeof v.produced_by_step==="string"||v.produced_by_step===null))&&(v.from_input_rows===undefined||(Array.isArray(v.from_input_rows)&&v.from_input_rows.every(x=>typeof x==="string")||v.from_input_rows===null))&&(v.build_receipt_id===undefined||(typeof v.build_receipt_id==="string"||v.build_receipt_id===null))&&(v.output_index===undefined||(Number.isFinite(v.output_index)||v.output_index===null));
|
|
30
|
+
const ownerCasRef=v=>typeof v==="string";
|
|
31
|
+
const ownerSynthesis=v=>shape(v,["answer","core","terminal"],[])&&(typeof v.answer==="string")&&(v.core!==null&&typeof v.core==="object"&&!Array.isArray(v.core)&&Object.values(v.core).every(x=>true))&&((v.terminal==="ok"||v.terminal==="partial"||v.terminal==="blocked"||v.terminal==="refused"||v.terminal==="infeasible_physics"||v.terminal==="capability_gap"));
|
|
32
|
+
export const validateAccount=v=>ownerAccount(v)&&[v.id,v.email,v.display_name,v.org_id].every(text)&&v.workspaces.every(w=>[w.id,w.org_id,w.name].every(text))&&integer(v.entitlements.active_projects.current)&&v.entitlements.active_projects.current>=0&&(v.entitlements.active_projects.maximum===null||(integer(v.entitlements.active_projects.maximum)&&v.entitlements.active_projects.maximum>=0));
|
|
33
|
+
export const validateQuotaDenial=v=>ownerQuotaDenial(v)&&v.resource==="active_projects"&&integer(v.current)&&v.current>=0&&integer(v.maximum)&&v.maximum>=0&&text(v.tier);
|
|
34
|
+
const plainText=v=>text(v)&&!/[\u0000-\u001f\u007f-\u009f]/.test(v); const costText=v=>v===null||plainText(v); const money=v=>v===null||(integer(v)&&v>=0);
|
|
35
|
+
const costProjection=v=>[v.scope?.id,v.binding_scope?.scope?.id].every(plainText)&&[v.period_start,v.period_end,v.reset_at].every(x=>integer(x)&&x>=0&&x<=8640000000000)&&v.period_start<v.period_end&&v.reset_at===v.period_end&&[v.used,v.limit,v.remaining].every(money)&&[v.display?.used,v.display?.limit,v.display?.remaining].every(costText)&&(v.coverage==="complete"?(v.used!==null&&v.display.used!==null&&((v.limit===null&&v.remaining===null&&v.display.limit===null&&v.display.remaining===null)||(v.limit!==null&&v.remaining!==null&&v.display.limit!==null&&v.display.remaining!==null))):(v.used===null&&v.limit===null&&v.remaining===null&&v.display.used===null&&v.display.limit===null&&v.display.remaining===null));
|
|
36
|
+
export const validateCustomerCostProjection=v=>ownerCustomerCostProjection(v)&&costProjection(v);
|
|
37
|
+
export const validateLimitDenial=v=>ownerLimitDenial(v)&&costProjection(v)&&v.recovery.length>0;
|
|
38
|
+
export const validateArtifact=v=>ownerArtifact(v)&&plainText(v.id)&&/^sha256:[0-9a-f]{64}$/.test(v.cas_ref)&&plainText(v.filename);
|
|
39
|
+
export const validateSynthesis=v=>ownerSynthesis(v)&&text(v.answer)&&!/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/.test(v.answer);
|
|
40
|
+
const member=(v)=>exact(v,["user_id","email","display_name","role"])&&[v.user_id,v.email,v.display_name].every(text)&&["OWNER","EDITOR","VIEWER"].includes(v.role);
|
|
41
|
+
export const validateProject=(v)=>exact(v,["id","name","org_id","workspace_id","role","members","created_at","updated_at"])&&[v.id,v.name,v.org_id,v.workspace_id].every(text)&&["OWNER","EDITOR","VIEWER"].includes(v.role)&&Array.isArray(v.members)&&v.members.every(member)&&integer(v.created_at)&&integer(v.updated_at);
|
|
42
|
+
export const validateRunAck=(v)=>exact(v,["run_id","state_hash","accepted_at","requested_route_id"])&&[v.run_id,v.state_hash,v.requested_route_id].every(text)&&integer(v.accepted_at);
|
|
43
|
+
const dimension=(v)=>shape(v,["uncached_input","cache_read","cache_write","output"],["cache_write_class","cache_write_duration_s","provider_tier","long_context_band"])&&["uncached_input","cache_read","cache_write","output"].every(k=>integer(v[k])&&v[k]>=0)&&[v.cache_write_class,v.provider_tier,v.long_context_band].every(x=>x===undefined||x===null||text(x))&&(v.cache_write_duration_s===undefined||v.cache_write_duration_s===null||(integer(v.cache_write_duration_s)&&v.cache_write_duration_s>=0));
|
|
44
|
+
const usage=(v)=>exact(v,["coverage","provider_id","model_id","observed_at","billable_dimensions"])&&["complete","incomplete"].includes(v.coverage)&&[v.provider_id,v.model_id].every(text)&&integer(v.observed_at)&&v.observed_at>=0&&Array.isArray(v.billable_dimensions)&&v.billable_dimensions.length>0&&v.billable_dimensions.every(dimension);
|
|
45
|
+
export const validateAdmitted=(v)=>shape(v,["run_id","state_hash","result_hash","requested_route_id","profile","terminal","usage","events"],["duration_seconds"])&&[v.run_id,v.state_hash,v.result_hash,v.requested_route_id,v.profile].every(text)&&["ok","partial","blocked","refused","infeasible_physics","capability_gap"].includes(v.terminal)&&usage(v.usage)&&Array.isArray(v.events)&&(v.duration_seconds===undefined||v.duration_seconds===null||(integer(v.duration_seconds)&&v.duration_seconds>=0));
|
|
46
|
+
export const validatePatCreated=(v)=>exact(v,["id","name","token","prefix","project_id","scopes","created_at","expires_at"])&&[v.id,v.name,v.token,v.prefix].every(text)&&(v.project_id===null||text(v.project_id))&&Array.isArray(v.scopes)&&v.scopes.every(s=>["project:read","project:execute"].includes(s))&&integer(v.created_at)&&integer(v.expires_at);
|
|
47
|
+
export const validateDeviceAuthorization=(v)=>exact(v,["device_code","user_code","verification_uri","verification_uri_complete","expires_at","interval"])&&[v.device_code,v.user_code,v.verification_uri,v.verification_uri_complete].every(text)&&integer(v.expires_at)&&integer(v.interval)&&v.interval>=0;
|