@vincentt-xr/cli 0.20.0 → 0.22.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.
Files changed (68) hide show
  1. package/MIGRATION.md +30 -0
  2. package/dist/cli/index.js +86 -12
  3. package/dist/domain/api.js +16 -12
  4. package/dist/domain/command.js +9 -0
  5. package/dist/dx/command.d.ts +74 -18
  6. package/dist/dx/command.js +140 -86
  7. package/dist/dx/consent.d.ts +25 -6
  8. package/dist/dx/consent.js +64 -35
  9. package/dist/dx/enablement.d.ts +63 -11
  10. package/dist/dx/enablement.js +93 -21
  11. package/dist/dx/report.d.ts +4 -24
  12. package/dist/dx/report.js +32 -25
  13. package/dist/dx/reportIo.js +6 -4
  14. package/dist/dx/send.js +6 -4
  15. package/dist/lib/backend.js +5 -10
  16. package/dist/lib/exitCodes.d.ts +6 -0
  17. package/dist/lib/exitCodes.js +6 -0
  18. package/dist/lib/terminalText.d.ts +6 -0
  19. package/dist/lib/terminalText.js +10 -0
  20. package/dist/lib/version.d.ts +28 -2
  21. package/dist/lib/version.js +30 -3
  22. package/dist/lib/whoami.js +3 -0
  23. package/dist/library/assetsCommand.js +4 -0
  24. package/dist/library/client.js +6 -8
  25. package/dist/login/authBackend.js +18 -15
  26. package/dist/login/loginCommand.js +31 -4
  27. package/dist/login/pkceLogin.d.ts +10 -0
  28. package/dist/login/pkceLogin.js +24 -1
  29. package/dist/platform/releaseFetch.js +36 -69
  30. package/dist/preview/api.js +9 -12
  31. package/dist/preview/command.d.ts +3 -2
  32. package/dist/preview/command.js +10 -5
  33. package/dist/preview/copy.d.ts +13 -1
  34. package/dist/preview/copy.js +31 -3
  35. package/dist/preview/proxy.js +5 -39
  36. package/dist/preview/runner.d.ts +31 -9
  37. package/dist/preview/runner.js +43 -15
  38. package/dist/publish/api.js +14 -14
  39. package/dist/publish/command.d.ts +3 -0
  40. package/dist/publish/command.js +74 -7
  41. package/dist/publish/copy.d.ts +15 -0
  42. package/dist/publish/copy.js +109 -19
  43. package/dist/publish/keyScan.d.ts +25 -0
  44. package/dist/publish/keyScan.js +88 -0
  45. package/dist/publish/manifest.d.ts +60 -19
  46. package/dist/publish/manifest.js +290 -71
  47. package/dist/publish/upload.d.ts +18 -0
  48. package/dist/publish/upload.js +38 -0
  49. package/dist/publish/weight.d.ts +23 -0
  50. package/dist/publish/weight.js +113 -0
  51. package/dist/shared/config.d.ts +6 -6
  52. package/dist/shared/credentialNames.d.ts +17 -0
  53. package/dist/shared/credentialNames.js +72 -0
  54. package/dist/shared/http.d.ts +34 -0
  55. package/dist/shared/http.js +98 -2
  56. package/dist/shared/upgradeError.d.ts +62 -0
  57. package/dist/shared/upgradeError.js +215 -0
  58. package/dist/tunnel/caps.d.ts +19 -0
  59. package/dist/tunnel/caps.js +52 -0
  60. package/dist/tunnel/cbor.d.ts +39 -12
  61. package/dist/tunnel/cbor.js +84 -10
  62. package/dist/tunnel/client.d.ts +56 -2
  63. package/dist/tunnel/client.js +160 -44
  64. package/dist/tunnel/frame.d.ts +1 -1
  65. package/dist/tunnel/frame.js +8 -4
  66. package/dist/tunnel/index.d.ts +3 -2
  67. package/dist/tunnel/index.js +3 -2
  68. package/package.json +4 -3
@@ -16,7 +16,9 @@ import { request as httpRequest } from "node:http";
16
16
  import { connect as netConnect } from "node:net";
17
17
  import { WebSocket } from "ws";
18
18
  import { FrameDecoder, FrameType, MAX_FRAME_LEN, ProtocolError, ResetReason, RELAY_BUF_SIZE, INITIAL_CREDIT, TUNNEL_PING_INTERVAL_MS, TUNNEL_PING_TIMEOUT_MS, decodeCredit, encodeCredit, encodeFrame, } from "./frame.js";
19
- import { decodeControl, decodeOpen, encodeHead, parseUAClass, parseViewerLabel, } from "./cbor.js";
19
+ import { BASE_CAPS, decodeControl, decodeOpen, encodeHead, parseUAClass, parseViewerLabel, } from "./cbor.js";
20
+ import { CAPS_HEADER, OFFERED_CAPS_VALUE, parseAgreedCaps } from "./caps.js";
21
+ import { CLIENT_HEADER, clientHeaderValue } from "../shared/clientHeader.js";
20
22
  import { FOLLOW_REDIRECTS, ForwardTargetError, assertStillLoopback, normalizeRequestTarget, sanitizeHeaders, } from "./forwardTarget.js";
21
23
  /** RECONNECT_BACKOFF_MS is 0.5s -> 8s with jitter (2.2). */
22
24
  const BACKOFF_BASE_MS = 500;
@@ -79,6 +81,19 @@ export const RESUME_HEADER = "x-vincentt-resume";
79
81
  export const SESSION_HEADER = "x-vincentt-session";
80
82
  /** The Authorization header, named once so the never-send-both rule cites it. */
81
83
  export const AUTHORIZATION_HEADER = "authorization";
84
+ /**
85
+ * The version refusal on a dial: `410` with these headers (f3 §2.2.3). 410 is
86
+ * in a released client's stop set; only a client that reads the reason header
87
+ * knows it means "upgrade".
88
+ */
89
+ export const REFUSAL_HEADER = "x-vincentt-refusal";
90
+ export const CLI_MINIMUM_HEADER = "x-vincentt-cli-minimum";
91
+ export const CLI_DECLARED_HEADER = "x-vincentt-cli-declared";
92
+ export const UPGRADE_REQUIRED_REFUSAL = "cli_upgrade_required";
93
+ /** One header value, or undefined when absent or not a single string. */
94
+ function single(v) {
95
+ return typeof v === "string" ? v : undefined;
96
+ }
82
97
  /**
83
98
  * RESUME_TOKEN_LEN is base64url(32 random bytes), unpadded. It mirrors
84
99
  * `resumeTokenLen` in the Go edge so the two ends of this wire state the same
@@ -104,22 +119,40 @@ export function parseResumeToken(raw) {
104
119
  return null;
105
120
  return RESUME_TOKEN_RE.test(raw) ? raw : null;
106
121
  }
122
+ /** The forwarded content-length, parsed as digits only. */
123
+ function declaredLength(headers) {
124
+ const values = headers
125
+ .filter(([k]) => k.toLowerCase() === "content-length")
126
+ .map(([, v]) => v);
127
+ if (values.length === 0)
128
+ return { kind: "none" };
129
+ if (values.length > 1 || !/^\d{1,15}$/.test(values[0]))
130
+ return { kind: "invalid" };
131
+ return { kind: "length", n: Number(values[0]) };
132
+ }
107
133
  /**
108
- * Whether the edge will stream a body for this request. The edge pumps DATA and
109
- * a closing END only when the request has one (`pumpRequestBody`, taken when
110
- * ContentLength != 0), so a request without a positive content-length gets no
111
- * END at all and must be ended locally at once, as before.
134
+ * With `open-body` agreed, the edge's `body` decides and a content-length must
135
+ * agree with it (the edge writes at most one, positive). Without it, the base
136
+ * rule: a positive content-length streams, anything else ends at OPEN. An
137
+ * upgrade has no body whatever it declares.
112
138
  */
113
- function declaresBody(headers) {
114
- return declaredLength(headers) > 0;
115
- }
116
- /** The forwarded content-length as a whole number, or 0 when absent or unusable. */
117
- function declaredLength(headers) {
118
- const raw = headers.find(([k]) => k.toLowerCase() === "content-length")?.[1];
119
- if (raw === undefined || !/^\s*\d+\s*$/.test(raw))
120
- return 0;
121
- const n = Number(raw.trim());
122
- return Number.isSafeInteger(n) ? n : 0;
139
+ export function bodyPlan(open, caps) {
140
+ if (open.upgrade !== undefined)
141
+ return { kind: "none" };
142
+ const cl = declaredLength(open.headers);
143
+ if (cl.kind === "invalid")
144
+ return { kind: "reset" };
145
+ if (caps.has("open-body")) {
146
+ if (open.body === true) {
147
+ if (cl.kind === "none")
148
+ return { kind: "chunked" };
149
+ return cl.n > 0 ? { kind: "declared", length: cl.n } : { kind: "reset" };
150
+ }
151
+ return cl.kind === "length" && cl.n > 0 ? { kind: "reset" } : { kind: "none" };
152
+ }
153
+ return cl.kind === "length" && cl.n > 0
154
+ ? { kind: "declared", length: cl.n }
155
+ : { kind: "none" };
123
156
  }
124
157
  export class TunnelClient {
125
158
  opts;
@@ -133,6 +166,8 @@ export class TunnelClient {
133
166
  * is a credential in a log.
134
167
  */
135
168
  resumeToken = null;
169
+ /** This connection's agreed set. Fixed at its 101, reset on every dial. */
170
+ caps = BASE_CAPS;
136
171
  attempt = 0;
137
172
  closed = false;
138
173
  state = "connecting";
@@ -182,14 +217,21 @@ export class TunnelClient {
182
217
  * a resume token only ever exists after a 101 has delivered one.
183
218
  */
184
219
  dialHeaders() {
220
+ // The client tag and the caps offer ride BOTH branches. Neither is a
221
+ // credential, so the never-send-both rule below is untouched by them.
222
+ const offer = {
223
+ [CLIENT_HEADER]: clientHeaderValue(),
224
+ [CAPS_HEADER]: OFFERED_CAPS_VALUE,
225
+ };
185
226
  if (this.resumeToken === null) {
186
227
  // FIRST ATTACH. The grant is a BEARER credential on the upgrade request,
187
228
  // never in the URL: a capability in a query string lands in every
188
229
  // intermediary's log.
189
- return { [AUTHORIZATION_HEADER]: `Bearer ${this.opts.grant}` };
230
+ return { ...offer, [AUTHORIZATION_HEADER]: `Bearer ${this.opts.grant}` };
190
231
  }
191
232
  // RECONNECT. The resume pair, and no Authorization header at all.
192
233
  return {
234
+ ...offer,
193
235
  [RESUME_HEADER]: this.resumeToken,
194
236
  [SESSION_HEADER]: this.opts.sessionId,
195
237
  };
@@ -207,6 +249,8 @@ export class TunnelClient {
207
249
  });
208
250
  this.ws = ws;
209
251
  this.decoder = new FrameDecoder(true);
252
+ // Until a 101 says otherwise, this connection speaks the base protocol.
253
+ this.caps = BASE_CAPS;
210
254
  // `ws` emits BOTH `error` and `close` for a single failed connection, so
211
255
  // the handler is bound once per socket and guarded: without the guard each
212
256
  // failure schedules TWO retries, the next failure four, and the backoff
@@ -234,6 +278,16 @@ export class TunnelClient {
234
278
  // later handshake omitted the header would forfeit a working reconnect.
235
279
  if (next !== null)
236
280
  this.resumeToken = next;
281
+ // The agreed set fixes this connection's grammar. Absent is the base
282
+ // protocol (an edge that predates negotiation); a malformed answer, or
283
+ // one naming a token this client did not offer, is the edge
284
+ // misbehaving and ends the preview.
285
+ const agreed = parseAgreedCaps(res.headers[CAPS_HEADER], res.rawHeaders);
286
+ if (agreed === null) {
287
+ this.fatalViolation(ws);
288
+ return;
289
+ }
290
+ this.caps = agreed;
237
291
  });
238
292
  /**
239
293
  * A REFUSED RECONNECT — the edge answered with a status instead of a 101.
@@ -259,6 +313,16 @@ export class TunnelClient {
259
313
  */
260
314
  ws.on("unexpected-response", (_req, res) => {
261
315
  const status = res.statusCode ?? 0;
316
+ // The version refusal: final, one dial, no backoff. It is reported
317
+ // instead of the generic refused-reconnect, whose 410 means "gone".
318
+ if (status === 410 &&
319
+ single(res.headers?.[REFUSAL_HEADER]) === UPGRADE_REQUIRED_REFUSAL) {
320
+ this.closed = true;
321
+ res.destroy?.();
322
+ this.opts.onRefused?.("upgrade_required", single(res.headers?.[CLI_MINIMUM_HEADER]), single(res.headers?.[CLI_DECLARED_HEADER]));
323
+ closeOnce();
324
+ return;
325
+ }
262
326
  this.opts.onReconnectRefused?.(status);
263
327
  // TERMINAL statuses stop the loop. A refused credential does not become
264
328
  // valid by being presented again, and re-dialling a 401 is what turns
@@ -317,6 +381,18 @@ export class TunnelClient {
317
381
  this.open();
318
382
  }, delay);
319
383
  }
384
+ /**
385
+ * End the preview for a violation the edge alone could cause. A redial reaches
386
+ * the same edge and gets the same answer: before this was terminal, a
387
+ * persistently malformed 101 redialled every 8 seconds indefinitely.
388
+ */
389
+ fatalViolation(ws) {
390
+ if (ws === null || this.ws !== ws || this.closed)
391
+ return;
392
+ this.closed = true;
393
+ this.opts.onProtocolViolation?.();
394
+ ws.close();
395
+ }
320
396
  startPing() {
321
397
  this.stopPing();
322
398
  this.pingTimer = setInterval(() => {
@@ -350,18 +426,19 @@ export class TunnelClient {
350
426
  try {
351
427
  frames = this.decoder.push(chunk);
352
428
  }
353
- catch (err) {
354
- // A FATAL protocol error closes the tunnel; a stream-level one cannot
355
- // reach here (the decoder only throws fatal ones).
356
- if (err instanceof ProtocolError && err.fatal) {
357
- this.ws?.close();
358
- return;
359
- }
429
+ catch {
430
+ // The frame decoder throws only fatal errors: the tunnel closes.
360
431
  this.ws?.close();
361
432
  return;
362
433
  }
363
- for (const f of frames)
434
+ const ws = this.ws;
435
+ for (const f of frames) {
436
+ // A fatal frame earlier in this chunk closed the connection; nothing
437
+ // after it is acted on.
438
+ if (this.ws !== ws || ws === null || ws.readyState !== WebSocket.OPEN)
439
+ return;
364
440
  this.dispatch(f.type, f.streamId, f.payload);
441
+ }
365
442
  }
366
443
  dispatch(type, streamId, payload) {
367
444
  switch (type) {
@@ -372,31 +449,41 @@ export class TunnelClient {
372
449
  this.lastPong = Date.now();
373
450
  return;
374
451
  case FrameType.CONTROL: {
452
+ let ctrl;
375
453
  try {
376
- const ctrl = decodeControl(payload);
377
- if (ctrl.kind === "viewer_gone") {
378
- // Routed AWAY from onControl deliberately. The other three kinds
379
- // all mean "this session is ending" and the CLI prints an ending
380
- // screen for them; a viewer leaving means the opposite — the
381
- // session is fine and one device went away.
382
- this.opts.onViewerGone?.(ctrl.viewerLabel);
383
- }
384
- else {
385
- this.opts.onControl?.(ctrl.kind);
386
- }
454
+ ctrl = decodeControl(payload, this.caps);
387
455
  }
388
456
  catch {
389
- // A malformed CONTROL is a stream-level fault on stream 0, which
390
- // means it is simply ignored rather than killing a live tunnel.
457
+ // A CONTROL this connection cannot read (malformed, unknown, or a
458
+ // kind it did not agree) is fatal: only the edge writes stream 0.
459
+ this.fatalViolation(this.ws);
460
+ return;
461
+ }
462
+ if (ctrl.kind === "viewer_gone") {
463
+ // Routed AWAY from onControl deliberately. The other kinds all mean
464
+ // "this session is ending" and the CLI prints an ending screen for
465
+ // them; a viewer leaving means the opposite — the session is fine
466
+ // and one device went away.
467
+ this.opts.onViewerGone?.(ctrl.viewerLabel);
468
+ return;
391
469
  }
470
+ // The edge drained this session and is about to close the socket. The
471
+ // session will not come back, so no redial may follow that close.
472
+ if (ctrl.kind === "authz_expired")
473
+ this.closed = true;
474
+ this.opts.onControl?.(ctrl.kind);
392
475
  return;
393
476
  }
394
477
  case FrameType.OPEN: {
395
478
  let open;
396
479
  try {
397
- open = decodeOpen(payload);
480
+ open = decodeOpen(payload, this.caps);
398
481
  }
399
- catch {
482
+ catch (err) {
483
+ if (err instanceof ProtocolError && err.fatal) {
484
+ this.fatalViolation(this.ws);
485
+ return;
486
+ }
400
487
  this.send(FrameType.RESET, streamId, new Uint8Array([ResetReason.PROTOCOL_ERROR]));
401
488
  return;
402
489
  }
@@ -435,6 +522,17 @@ export class TunnelClient {
435
522
  }
436
523
  case FrameType.END: {
437
524
  const s = this.streams.get(streamId);
525
+ // An END is bare (half-close) or a u24 credit grant. Any other length
526
+ // resets that stream; the tunnel survives.
527
+ if (payload.length !== 0 && payload.length !== 3) {
528
+ if (s !== undefined) {
529
+ this.quietReset(streamId, s, 0, ResetReason.PROTOCOL_ERROR);
530
+ }
531
+ else {
532
+ this.send(FrameType.RESET, streamId, new Uint8Array([ResetReason.PROTOCOL_ERROR]));
533
+ }
534
+ return;
535
+ }
438
536
  if (s === undefined)
439
537
  return;
440
538
  if (payload.length === 3) {
@@ -497,12 +595,19 @@ export class TunnelClient {
497
595
  const uaClass = parseUAClass(open.headers.find(([k]) => k.toLowerCase() === "x-vincentt-ua-class")?.[1] ??
498
596
  "unknown");
499
597
  this.opts.onViewer?.(label, uaClass);
598
+ // A content-length that disagrees with what the edge said is viewer-made,
599
+ // so it resets this stream only. No local request is opened for it.
600
+ const plan = bodyPlan(open, this.caps);
601
+ if (plan.kind === "reset") {
602
+ this.send(FrameType.RESET, streamId, new Uint8Array([ResetReason.PROTOCOL_ERROR]));
603
+ return;
604
+ }
500
605
  // A request with a body is registered BEFORE the loopback await below: the
501
606
  // edge sends DATA right behind OPEN, and a frame for an unknown stream is
502
607
  // answered with RESET. Bytes that arrive before the local request exists
503
608
  // are held here. They are bounded by the edge's credit window, because
504
609
  // credit is returned only once the local server has taken them.
505
- const expectsBody = open.upgrade === undefined && declaresBody(open.headers);
610
+ const expectsBody = plan.kind !== "none";
506
611
  const early = {
507
612
  pending: [],
508
613
  ended: false,
@@ -545,11 +650,18 @@ export class TunnelClient {
545
650
  // Reset or torn down while the loopback check ran: nothing to open.
546
651
  if (early.aborted)
547
652
  return;
653
+ // sanitizeHeaders drops every viewer `x-vincentt-*` and `transfer-encoding`
654
+ // before anything of ours is stamped.
548
655
  const headers = sanitizeHeaders(open.headers, this.opts.target);
549
656
  // Stamped AFTER sanitizeHeaders so a viewer-supplied header of the same
550
657
  // name cannot shadow ours. Both values are already validated: the label
551
658
  // against VIEWER_LABEL_RE, the class against the closed enum of six.
552
659
  headers.push([VIEWER_STAMP_HEADER, label], [UA_CLASS_STAMP_HEADER, uaClass]);
660
+ // Explicit, whatever the method: Node frames no GET, HEAD, DELETE or
661
+ // OPTIONS body on its own, and unframed bytes on the loopback hop would be
662
+ // parsed as a second request that bypassed every rule above.
663
+ if (plan.kind === "chunked")
664
+ headers.push(["transfer-encoding", "chunked"]);
553
665
  this.streams.set(streamId, stream);
554
666
  if (open.upgrade !== undefined) {
555
667
  // `upgrade` is CARRIED, NOT INTERPRETED, by the protocol — but the local
@@ -565,6 +677,9 @@ export class TunnelClient {
565
677
  method: open.method,
566
678
  path,
567
679
  headers: Object.fromEntries(headers),
680
+ // One loopback connection per stream: no connection, and so no
681
+ // leftover bytes, outlives the stream that opened it.
682
+ agent: false,
568
683
  // The BROWSER follows a redirect, on the viewer's own origin. There is
569
684
  // no option to change this.
570
685
  ...(FOLLOW_REDIRECTS ? {} : {}),
@@ -621,11 +736,11 @@ export class TunnelClient {
621
736
  // A body that disagrees with its content-length, long or short, ends in a
622
737
  // quiet RESET: extra bytes never reach the socket (they could be smuggled
623
738
  // onto a reused connection), and a short body never leaves the local server
624
- // waiting for bytes that will not come.
625
- const declared = declaredLength(open.headers);
739
+ // waiting for bytes that will not come. A chunked body has no count to hold.
740
+ const declared = plan.kind === "declared" ? plan.length : undefined;
626
741
  let written = 0;
627
742
  stream.writeBody = (b) => {
628
- if (written + b.length > declared) {
743
+ if (declared !== undefined && written + b.length > declared) {
629
744
  // Thrown to the DATA handler, which resets quietly; nothing is written.
630
745
  throw new Error("body exceeds content-length");
631
746
  }
@@ -641,8 +756,9 @@ export class TunnelClient {
641
756
  // The body ends on the edge's half-close, never at OPEN, and only when every
642
757
  // declared byte arrived. Thrown to the caller, which resets quietly.
643
758
  stream.endBody = () => {
644
- if (written !== declared)
759
+ if (declared !== undefined && written !== declared) {
645
760
  throw new Error("body shorter than content-length");
761
+ }
646
762
  req.end();
647
763
  };
648
764
  try {
@@ -71,7 +71,7 @@ export declare class ProtocolError extends Error {
71
71
  */
72
72
  constructor(code: ProtocolErrorCode, fatal: boolean);
73
73
  }
74
- export type ProtocolErrorCode = "frame_too_large" | "unknown_frame_type" | "even_stream_id" | "zero_stream_id" | "nonzero_stream_id" | "short_payload" | "open_payload_too_large" | "cbor_malformed" | "cbor_depth" | "cbor_headers" | "cbor_header_bytes" | "cbor_path_len" | "cbor_status" | "cbor_viewer";
74
+ export type ProtocolErrorCode = "frame_too_large" | "unknown_frame_type" | "even_stream_id" | "zero_stream_id" | "nonzero_stream_id" | "short_payload" | "open_payload_too_large" | "cbor_malformed" | "cbor_depth" | "cbor_headers" | "cbor_header_bytes" | "cbor_path_len" | "cbor_status" | "cbor_viewer" | "not_negotiated" | "open_body_missing" | "open_body_with_upgrade";
75
75
  export interface FrameHeader {
76
76
  type: FrameType;
77
77
  streamId: number;
@@ -81,7 +81,7 @@ export class ProtocolError extends Error {
81
81
  this.name = "ProtocolError";
82
82
  }
83
83
  }
84
- /** The fatal set, matching Go's TunnelFatal exactly. */
84
+ /** Every code that closes the tunnel rather than one stream. */
85
85
  const FATAL = new Set([
86
86
  "frame_too_large",
87
87
  "unknown_frame_type",
@@ -89,6 +89,9 @@ const FATAL = new Set([
89
89
  "zero_stream_id",
90
90
  "nonzero_stream_id",
91
91
  "short_payload",
92
+ "not_negotiated",
93
+ "open_body_missing",
94
+ "open_body_with_upgrade",
92
95
  ]);
93
96
  function fail(code) {
94
97
  throw new ProtocolError(code, FATAL.has(code));
@@ -125,9 +128,10 @@ export function validateFrameShape(type, streamId, length, fromEdge) {
125
128
  if (length > MAX_FRAME_LEN)
126
129
  fail("frame_too_large");
127
130
  if (!KNOWN_TYPES.has(type)) {
128
- // NOT ignored. "Ignore unknown for forward-compat" is explicitly REJECTED:
129
- // this is a closed protocol between two implementations we both own, and
130
- // tolerance here would be a place to smuggle bytes.
131
+ // A frame type outside the set AGREED FOR THIS CONNECTION closes it. No
132
+ // negotiated element is a frame type, so the agreed set of types is the base
133
+ // set on every connection. Negotiation narrows what each side may send; it
134
+ // never widens what a receiver accepts, so an unknown type is never ignored.
131
135
  fail("unknown_frame_type");
132
136
  }
133
137
  if (streamTyped(type)) {
@@ -4,6 +4,7 @@
4
4
  * fix ships as a CLI release.
5
5
  */
6
6
  export { FrameDecoder, FrameType, ProtocolError, ResetReason, FRAME_HEADER_LEN, IDLE_STREAM_TIMEOUT_MS, INITIAL_CREDIT, MAX_CBOR_DEPTH, MAX_CONCURRENT_STREAMS, MAX_FRAME_LEN, MAX_HEADERS, MAX_HEADER_BYTES, MAX_OPEN_PAYLOAD, MAX_PATH_LEN, RELAY_BUF_SIZE, TUNNEL_PING_INTERVAL_MS, TUNNEL_PING_TIMEOUT_MS, decodeCredit, decodeHeader, encodeCredit, encodeFrame, encodeHeader, validateFrameShape, type Frame, type FrameHeader, type ProtocolErrorCode, } from "./frame.js";
7
- export { UA_CLASSES, VIEWER_LABEL_RE, decodeControl, decodeHead, decodeOpen, encodeControl, encodeHead, encodeOpen, parseUAClass, parseViewerLabel, type ControlKind, type ControlPayload, type HeadPayload, type OpenPayload, type UAClass, } from "./cbor.js";
7
+ export { BASE_CAPS, TUNNEL_CAPS, UA_CLASSES, VIEWER_LABEL_RE, decodeControl, decodeHead, decodeOpen, encodeControl, encodeHead, encodeOpen, parseUAClass, parseViewerLabel, type CapSet, type ControlKind, type ControlPayload, type HeadPayload, type OpenPayload, type TunnelCap, type UAClass, } from "./cbor.js";
8
+ export { CAPS_HEADER, OFFERED_CAPS, OFFERED_CAPS_VALUE, parseAgreedCaps } from "./caps.js";
8
9
  export { FOLLOW_REDIRECTS, ForwardTargetError, assertStillLoopback, normalizeRequestTarget, resolveForwardTarget, sanitizeHeaders, type ForwardTarget, type ForwardTargetErrorCode, } from "./forwardTarget.js";
9
- export { TunnelClient, RESUME_HEADER, RESUME_TOKEN_LEN, UA_CLASS_STAMP_HEADER, VIEWER_STAMP_HEADER, parseResumeToken, type TunnelClientOptions, type TunnelState, } from "./client.js";
10
+ export { TunnelClient, CLI_DECLARED_HEADER, CLI_MINIMUM_HEADER, REFUSAL_HEADER, RESUME_HEADER, RESUME_TOKEN_LEN, UPGRADE_REQUIRED_REFUSAL, bodyPlan, type BodyPlan, UA_CLASS_STAMP_HEADER, VIEWER_STAMP_HEADER, parseResumeToken, type TunnelClientOptions, type TunnelState, } from "./client.js";
@@ -4,6 +4,7 @@
4
4
  * fix ships as a CLI release.
5
5
  */
6
6
  export { FrameDecoder, FrameType, ProtocolError, ResetReason, FRAME_HEADER_LEN, IDLE_STREAM_TIMEOUT_MS, INITIAL_CREDIT, MAX_CBOR_DEPTH, MAX_CONCURRENT_STREAMS, MAX_FRAME_LEN, MAX_HEADERS, MAX_HEADER_BYTES, MAX_OPEN_PAYLOAD, MAX_PATH_LEN, RELAY_BUF_SIZE, TUNNEL_PING_INTERVAL_MS, TUNNEL_PING_TIMEOUT_MS, decodeCredit, decodeHeader, encodeCredit, encodeFrame, encodeHeader, validateFrameShape, } from "./frame.js";
7
- export { UA_CLASSES, VIEWER_LABEL_RE, decodeControl, decodeHead, decodeOpen, encodeControl, encodeHead, encodeOpen, parseUAClass, parseViewerLabel, } from "./cbor.js";
7
+ export { BASE_CAPS, TUNNEL_CAPS, UA_CLASSES, VIEWER_LABEL_RE, decodeControl, decodeHead, decodeOpen, encodeControl, encodeHead, encodeOpen, parseUAClass, parseViewerLabel, } from "./cbor.js";
8
+ export { CAPS_HEADER, OFFERED_CAPS, OFFERED_CAPS_VALUE, parseAgreedCaps } from "./caps.js";
8
9
  export { FOLLOW_REDIRECTS, ForwardTargetError, assertStillLoopback, normalizeRequestTarget, resolveForwardTarget, sanitizeHeaders, } from "./forwardTarget.js";
9
- export { TunnelClient, RESUME_HEADER, RESUME_TOKEN_LEN, UA_CLASS_STAMP_HEADER, VIEWER_STAMP_HEADER, parseResumeToken, } from "./client.js";
10
+ export { TunnelClient, CLI_DECLARED_HEADER, CLI_MINIMUM_HEADER, REFUSAL_HEADER, RESUME_HEADER, RESUME_TOKEN_LEN, UPGRADE_REQUIRED_REFUSAL, bodyPlan, UA_CLASS_STAMP_HEADER, VIEWER_STAMP_HEADER, parseResumeToken, } from "./client.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincentt-xr/cli",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Vincentt CLI — the vincentt command-line dev loop (login, create, publish, preview, diagnostics, feedback, relay)",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -28,11 +28,12 @@
28
28
  "qrcode": "^1.5.4",
29
29
  "ws": "^8.18.0",
30
30
  "zod": "^3.23.8",
31
- "@vincentt-xr/harness": "2.0.0"
31
+ "@vincentt-xr/harness": "3.0.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/qrcode": "^1.5.5",
35
- "@types/ws": "^8.5.13"
35
+ "@types/ws": "^8.5.13",
36
+ "@vincentt-xr/cli-released": "npm:@vincentt-xr/cli@0.20.0"
36
37
  },
37
38
  "scripts": {
38
39
  "build": "tsc -p tsconfig.build.json",