@vincentt-xr/cli 0.19.0 → 0.21.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 (67) hide show
  1. package/MIGRATION.md +25 -0
  2. package/README.md +3 -2
  3. package/dist/cli/index.js +95 -13
  4. package/dist/cli/usage-text.js +3 -2
  5. package/dist/domain/api.js +16 -12
  6. package/dist/domain/command.js +9 -0
  7. package/dist/dx/command.js +3 -0
  8. package/dist/dx/consent.js +10 -14
  9. package/dist/dx/reportIo.js +6 -4
  10. package/dist/dx/send.js +6 -4
  11. package/dist/lib/backend.js +5 -10
  12. package/dist/lib/exitCodes.d.ts +6 -0
  13. package/dist/lib/exitCodes.js +6 -0
  14. package/dist/lib/terminalText.d.ts +6 -0
  15. package/dist/lib/terminalText.js +10 -0
  16. package/dist/lib/whoami.js +3 -0
  17. package/dist/library/assetsCommand.js +4 -0
  18. package/dist/library/client.js +6 -8
  19. package/dist/login/authBackend.d.ts +27 -2
  20. package/dist/login/authBackend.js +86 -17
  21. package/dist/login/loginCommand.d.ts +35 -3
  22. package/dist/login/loginCommand.js +81 -5
  23. package/dist/login/machineLabel.d.ts +46 -0
  24. package/dist/login/machineLabel.js +119 -0
  25. package/dist/login/pkceLogin.d.ts +17 -1
  26. package/dist/login/pkceLogin.js +30 -5
  27. package/dist/platform/releaseFetch.js +36 -69
  28. package/dist/preview/api.js +9 -12
  29. package/dist/preview/command.d.ts +3 -2
  30. package/dist/preview/command.js +8 -5
  31. package/dist/preview/copy.d.ts +8 -1
  32. package/dist/preview/copy.js +17 -4
  33. package/dist/preview/proxy.js +5 -39
  34. package/dist/preview/runner.d.ts +25 -9
  35. package/dist/preview/runner.js +42 -15
  36. package/dist/publish/api.js +14 -14
  37. package/dist/publish/command.d.ts +3 -0
  38. package/dist/publish/command.js +71 -7
  39. package/dist/publish/copy.d.ts +9 -0
  40. package/dist/publish/copy.js +83 -14
  41. package/dist/publish/keyScan.d.ts +25 -0
  42. package/dist/publish/keyScan.js +88 -0
  43. package/dist/publish/manifest.d.ts +51 -19
  44. package/dist/publish/manifest.js +246 -63
  45. package/dist/publish/upload.d.ts +8 -0
  46. package/dist/publish/upload.js +15 -0
  47. package/dist/publish/weight.d.ts +23 -0
  48. package/dist/publish/weight.js +113 -0
  49. package/dist/shared/authError.d.ts +1 -1
  50. package/dist/shared/authError.js +3 -1
  51. package/dist/shared/credentialNames.d.ts +17 -0
  52. package/dist/shared/credentialNames.js +72 -0
  53. package/dist/shared/http.d.ts +34 -0
  54. package/dist/shared/http.js +98 -2
  55. package/dist/shared/upgradeError.d.ts +62 -0
  56. package/dist/shared/upgradeError.js +215 -0
  57. package/dist/tunnel/caps.d.ts +19 -0
  58. package/dist/tunnel/caps.js +52 -0
  59. package/dist/tunnel/cbor.d.ts +39 -12
  60. package/dist/tunnel/cbor.js +84 -10
  61. package/dist/tunnel/client.d.ts +48 -2
  62. package/dist/tunnel/client.js +157 -44
  63. package/dist/tunnel/frame.d.ts +1 -1
  64. package/dist/tunnel/frame.js +8 -4
  65. package/dist/tunnel/index.d.ts +3 -2
  66. package/dist/tunnel/index.js +3 -2
  67. 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 closes the connection.
285
+ const agreed = parseAgreedCaps(res.headers[CAPS_HEADER], res.rawHeaders);
286
+ if (agreed === null) {
287
+ this.protocolViolation(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,15 @@ export class TunnelClient {
317
381
  this.open();
318
382
  }, delay);
319
383
  }
384
+ /**
385
+ * Close this connection for a violation the edge alone could cause. The
386
+ * ordinary close path then decides whether to redial.
387
+ */
388
+ protocolViolation(ws) {
389
+ if (this.ws !== ws)
390
+ return;
391
+ ws.close();
392
+ }
320
393
  startPing() {
321
394
  this.stopPing();
322
395
  this.pingTimer = setInterval(() => {
@@ -350,18 +423,19 @@ export class TunnelClient {
350
423
  try {
351
424
  frames = this.decoder.push(chunk);
352
425
  }
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
- }
426
+ catch {
427
+ // The frame decoder throws only fatal errors: the tunnel closes.
360
428
  this.ws?.close();
361
429
  return;
362
430
  }
363
- for (const f of frames)
431
+ const ws = this.ws;
432
+ for (const f of frames) {
433
+ // A fatal frame earlier in this chunk closed the connection; nothing
434
+ // after it is acted on.
435
+ if (this.ws !== ws || ws === null || ws.readyState !== WebSocket.OPEN)
436
+ return;
364
437
  this.dispatch(f.type, f.streamId, f.payload);
438
+ }
365
439
  }
366
440
  dispatch(type, streamId, payload) {
367
441
  switch (type) {
@@ -372,31 +446,41 @@ export class TunnelClient {
372
446
  this.lastPong = Date.now();
373
447
  return;
374
448
  case FrameType.CONTROL: {
449
+ let ctrl;
375
450
  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
- }
451
+ ctrl = decodeControl(payload, this.caps);
387
452
  }
388
453
  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.
454
+ // A CONTROL this connection cannot read (malformed, unknown, or a
455
+ // kind it did not agree) is fatal: only the edge writes stream 0.
456
+ this.ws?.close();
457
+ return;
458
+ }
459
+ if (ctrl.kind === "viewer_gone") {
460
+ // Routed AWAY from onControl deliberately. The other kinds all mean
461
+ // "this session is ending" and the CLI prints an ending screen for
462
+ // them; a viewer leaving means the opposite — the session is fine
463
+ // and one device went away.
464
+ this.opts.onViewerGone?.(ctrl.viewerLabel);
465
+ return;
391
466
  }
467
+ // The edge drained this session and is about to close the socket. The
468
+ // session will not come back, so no redial may follow that close.
469
+ if (ctrl.kind === "authz_expired")
470
+ this.closed = true;
471
+ this.opts.onControl?.(ctrl.kind);
392
472
  return;
393
473
  }
394
474
  case FrameType.OPEN: {
395
475
  let open;
396
476
  try {
397
- open = decodeOpen(payload);
477
+ open = decodeOpen(payload, this.caps);
398
478
  }
399
- catch {
479
+ catch (err) {
480
+ if (err instanceof ProtocolError && err.fatal) {
481
+ this.ws?.close();
482
+ return;
483
+ }
400
484
  this.send(FrameType.RESET, streamId, new Uint8Array([ResetReason.PROTOCOL_ERROR]));
401
485
  return;
402
486
  }
@@ -435,6 +519,17 @@ export class TunnelClient {
435
519
  }
436
520
  case FrameType.END: {
437
521
  const s = this.streams.get(streamId);
522
+ // An END is bare (half-close) or a u24 credit grant. Any other length
523
+ // resets that stream; the tunnel survives.
524
+ if (payload.length !== 0 && payload.length !== 3) {
525
+ if (s !== undefined) {
526
+ this.quietReset(streamId, s, 0, ResetReason.PROTOCOL_ERROR);
527
+ }
528
+ else {
529
+ this.send(FrameType.RESET, streamId, new Uint8Array([ResetReason.PROTOCOL_ERROR]));
530
+ }
531
+ return;
532
+ }
438
533
  if (s === undefined)
439
534
  return;
440
535
  if (payload.length === 3) {
@@ -497,12 +592,19 @@ export class TunnelClient {
497
592
  const uaClass = parseUAClass(open.headers.find(([k]) => k.toLowerCase() === "x-vincentt-ua-class")?.[1] ??
498
593
  "unknown");
499
594
  this.opts.onViewer?.(label, uaClass);
595
+ // A content-length that disagrees with what the edge said is viewer-made,
596
+ // so it resets this stream only. No local request is opened for it.
597
+ const plan = bodyPlan(open, this.caps);
598
+ if (plan.kind === "reset") {
599
+ this.send(FrameType.RESET, streamId, new Uint8Array([ResetReason.PROTOCOL_ERROR]));
600
+ return;
601
+ }
500
602
  // A request with a body is registered BEFORE the loopback await below: the
501
603
  // edge sends DATA right behind OPEN, and a frame for an unknown stream is
502
604
  // answered with RESET. Bytes that arrive before the local request exists
503
605
  // are held here. They are bounded by the edge's credit window, because
504
606
  // credit is returned only once the local server has taken them.
505
- const expectsBody = open.upgrade === undefined && declaresBody(open.headers);
607
+ const expectsBody = plan.kind !== "none";
506
608
  const early = {
507
609
  pending: [],
508
610
  ended: false,
@@ -545,11 +647,18 @@ export class TunnelClient {
545
647
  // Reset or torn down while the loopback check ran: nothing to open.
546
648
  if (early.aborted)
547
649
  return;
650
+ // sanitizeHeaders drops every viewer `x-vincentt-*` and `transfer-encoding`
651
+ // before anything of ours is stamped.
548
652
  const headers = sanitizeHeaders(open.headers, this.opts.target);
549
653
  // Stamped AFTER sanitizeHeaders so a viewer-supplied header of the same
550
654
  // name cannot shadow ours. Both values are already validated: the label
551
655
  // against VIEWER_LABEL_RE, the class against the closed enum of six.
552
656
  headers.push([VIEWER_STAMP_HEADER, label], [UA_CLASS_STAMP_HEADER, uaClass]);
657
+ // Explicit, whatever the method: Node frames no GET, HEAD, DELETE or
658
+ // OPTIONS body on its own, and unframed bytes on the loopback hop would be
659
+ // parsed as a second request that bypassed every rule above.
660
+ if (plan.kind === "chunked")
661
+ headers.push(["transfer-encoding", "chunked"]);
553
662
  this.streams.set(streamId, stream);
554
663
  if (open.upgrade !== undefined) {
555
664
  // `upgrade` is CARRIED, NOT INTERPRETED, by the protocol — but the local
@@ -565,6 +674,9 @@ export class TunnelClient {
565
674
  method: open.method,
566
675
  path,
567
676
  headers: Object.fromEntries(headers),
677
+ // One loopback connection per stream: no connection, and so no
678
+ // leftover bytes, outlives the stream that opened it.
679
+ agent: false,
568
680
  // The BROWSER follows a redirect, on the viewer's own origin. There is
569
681
  // no option to change this.
570
682
  ...(FOLLOW_REDIRECTS ? {} : {}),
@@ -621,11 +733,11 @@ export class TunnelClient {
621
733
  // A body that disagrees with its content-length, long or short, ends in a
622
734
  // quiet RESET: extra bytes never reach the socket (they could be smuggled
623
735
  // 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);
736
+ // waiting for bytes that will not come. A chunked body has no count to hold.
737
+ const declared = plan.kind === "declared" ? plan.length : undefined;
626
738
  let written = 0;
627
739
  stream.writeBody = (b) => {
628
- if (written + b.length > declared) {
740
+ if (declared !== undefined && written + b.length > declared) {
629
741
  // Thrown to the DATA handler, which resets quietly; nothing is written.
630
742
  throw new Error("body exceeds content-length");
631
743
  }
@@ -641,8 +753,9 @@ export class TunnelClient {
641
753
  // The body ends on the edge's half-close, never at OPEN, and only when every
642
754
  // declared byte arrived. Thrown to the caller, which resets quietly.
643
755
  stream.endBody = () => {
644
- if (written !== declared)
756
+ if (declared !== undefined && written !== declared) {
645
757
  throw new Error("body shorter than content-length");
758
+ }
646
759
  req.end();
647
760
  };
648
761
  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.19.0",
3
+ "version": "0.21.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",