@vecteur/cli 0.3.1 → 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/src/client.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
 
3
- import { EVENT_KINDS, validateAdmitted, validateProject, validateRunAck } from "./contract.js";
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, with the environment winning.
60
+ * Where a run gets its origin and its token, in one place.
33
61
  *
34
- * The environment first, because that is how CI and a container pass a credential and neither
35
- * should have to write a file to do it. The stored login second, so a human logs in once —
36
- * SRD 06's release bar for this door is "one persistent login, the same product from any
37
- * working directory", and reading only the environment made "logged in" last one shell.
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
- * Both are all-or-nothing per field: a stored login does not lend its token to an environment
40
- * that names a different origin, because a token minted for one deployment silently pointed at
41
- * another is a credential leak with a good explanation.
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
- export function configuration(env = process.env, stored = undefined) {
44
- const saved = stored === undefined ? readCredentials(env) : stored;
45
- const token = env.VECTEUR_TOKEN ?? saved?.token ?? "";
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.origin} and VECTEUR_BASE_URL names `
57
- + `${env.VECTEUR_BASE_URL} — set VECTEUR_TOKEN for that origin, or log in to it`);
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
- if (!/^vct_[A-Za-z0-9_-]{43}$/.test(token)) {
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 --url <origin>`, or set VECTEUR_TOKEN");
120
+ : "no login found: run `vecteur login`, or set VECTEUR_TOKEN");
64
121
  }
65
- const raw = env.VECTEUR_BASE_URL ?? saved?.origin ?? "";
66
- let url;
67
- try {
68
- url = new URL(raw);
69
- } catch {
70
- throw new CliError("origin_invalid", "VECTEUR_BASE_URL must be an absolute public origin");
71
- }
72
- const lexicalLoopback = /^http:\/\/(?:127\.0\.0\.1|localhost|\[::1\])(?::\d{1,5})?$/.test(raw);
73
- if ((url.protocol !== "https:" && !lexicalLoopback) ||
74
- url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
75
- throw new CliError("origin_invalid", "VECTEUR_BASE_URL must be HTTPS or an exact loopback HTTP origin");
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 timeout = Number(env.VECTEUR_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS);
78
- if (!Number.isInteger(timeout) || timeout < 100 || timeout > 120_000) {
79
- throw new CliError("timeout_invalid", "VECTEUR_TIMEOUT_MS must be 100 to 120000");
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
- Authorization: `Bearer ${this.config.token}`,
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
- const candidate = parseOwnerJson(await boundedText(response)).error?.code;
257
- if (typeof candidate === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(candidate)) {
258
- code = candidate;
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
- throw new CliError(code, `Public API refused request with HTTP ${response.status}`);
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", "Public API request timed out");
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", "Public API 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
- return this.runEvents(value.run_id, onEvent, signal);
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", "Public API request timed out");
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 { return refuseSecretValue(parseOwnerJson(await boundedText(response))); }
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 OWNER_CONTRACT_SHA256 = "a8a1bfec596231c560f80e9f9dbbc38f4b9dff0eef7e87db5bc4687f98b89597";
3
- 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"]);
4
- export const ORIGIN_KINDS = Object.freeze(["user","hypothesis","derived","file","catalog","default"]);
5
- export const ORIGIN_WORD = Object.freeze({"user":"your brief","hypothesis":"assumed","derived":"derived","file":"your file","catalog":"catalogue","default":"default","unattributed":"unstated"});
6
- const exact=(v,k)=>!!v&&typeof v==="object"&&!Array.isArray(v)&&Object.keys(v).sort().join("\n")===[...k].sort().join("\n");
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 member=(v)=>exact(v,["user_id","email","display_name","role"])&&[v.user_id,v.email,v.display_name].every(text)&&["OWNER","ADMIN","EDITOR","VIEWER"].includes(v.role);
9
- export const validateProject=(v)=>exact(v,["id","name","org_id","workspace_id","role","role_source","members","created_at","updated_at"])&&[v.id,v.name,v.org_id,v.workspace_id].every(text)&&["OWNER","ADMIN","EDITOR","VIEWER"].includes(v.role)&&["workspace","project"].includes(v.role_source)&&Array.isArray(v.members)&&v.members.every(member)&&integer(v.created_at)&&integer(v.updated_at);
10
- export const validateRunAck=(v)=>exact(v,["run_id","state_hash","accepted_at","profile"])&&[v.run_id,v.state_hash,v.profile].every(text)&&integer(v.accepted_at);
11
- const usage=(v)=>exact(v,["state","provider","model_id","input_tokens","output_tokens","cost_usd"])&&["recorded","usage_unavailable"].includes(v.state)&&[v.provider,v.model_id].every(text)&&[v.input_tokens,v.output_tokens].every(x=>x===null||(integer(x)&&x>=0))&&(v.cost_usd===null||(typeof v.cost_usd==="number"&&Number.isFinite(v.cost_usd)&&v.cost_usd>=0));
12
- export const validateAdmitted=(v)=>exact(v,["run_id","state_hash","result_hash","profile","terminal","usage","events"])&&[v.run_id,v.state_hash,v.result_hash,v.profile].every(text)&&["ok","partial","blocked","refused","infeasible_physics","capability_gap"].includes(v.terminal)&&usage(v.usage)&&Array.isArray(v.events);
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;