@vincentt-xr/cli 0.20.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.
- package/MIGRATION.md +15 -0
- package/dist/cli/index.js +63 -6
- package/dist/domain/api.js +16 -12
- package/dist/domain/command.js +9 -0
- package/dist/dx/command.js +3 -0
- package/dist/dx/consent.js +10 -14
- package/dist/dx/reportIo.js +6 -4
- package/dist/dx/send.js +6 -4
- package/dist/lib/backend.js +5 -10
- package/dist/lib/exitCodes.d.ts +6 -0
- package/dist/lib/exitCodes.js +6 -0
- package/dist/lib/terminalText.d.ts +6 -0
- package/dist/lib/terminalText.js +10 -0
- package/dist/lib/whoami.js +3 -0
- package/dist/library/assetsCommand.js +4 -0
- package/dist/library/client.js +6 -8
- package/dist/login/authBackend.js +18 -15
- package/dist/login/loginCommand.js +31 -4
- package/dist/login/pkceLogin.d.ts +10 -0
- package/dist/login/pkceLogin.js +24 -1
- package/dist/platform/releaseFetch.js +36 -69
- package/dist/preview/api.js +9 -12
- package/dist/preview/command.d.ts +3 -2
- package/dist/preview/command.js +8 -5
- package/dist/preview/copy.d.ts +8 -1
- package/dist/preview/copy.js +16 -3
- package/dist/preview/proxy.js +5 -39
- package/dist/preview/runner.d.ts +25 -9
- package/dist/preview/runner.js +42 -15
- package/dist/publish/api.js +14 -14
- package/dist/publish/command.d.ts +3 -0
- package/dist/publish/command.js +71 -7
- package/dist/publish/copy.d.ts +9 -0
- package/dist/publish/copy.js +83 -14
- package/dist/publish/keyScan.d.ts +25 -0
- package/dist/publish/keyScan.js +88 -0
- package/dist/publish/manifest.d.ts +51 -19
- package/dist/publish/manifest.js +246 -63
- package/dist/publish/upload.d.ts +8 -0
- package/dist/publish/upload.js +15 -0
- package/dist/publish/weight.d.ts +23 -0
- package/dist/publish/weight.js +113 -0
- package/dist/shared/credentialNames.d.ts +17 -0
- package/dist/shared/credentialNames.js +72 -0
- package/dist/shared/http.d.ts +34 -0
- package/dist/shared/http.js +98 -2
- package/dist/shared/upgradeError.d.ts +62 -0
- package/dist/shared/upgradeError.js +215 -0
- package/dist/tunnel/caps.d.ts +19 -0
- package/dist/tunnel/caps.js +52 -0
- package/dist/tunnel/cbor.d.ts +39 -12
- package/dist/tunnel/cbor.js +84 -10
- package/dist/tunnel/client.d.ts +48 -2
- package/dist/tunnel/client.js +157 -44
- package/dist/tunnel/frame.d.ts +1 -1
- package/dist/tunnel/frame.js +8 -4
- package/dist/tunnel/index.d.ts +3 -2
- package/dist/tunnel/index.js +3 -2
- package/package.json +4 -3
package/dist/tunnel/client.js
CHANGED
|
@@ -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
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
354
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
390
|
-
//
|
|
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 =
|
|
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 =
|
|
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 {
|
package/dist/tunnel/frame.d.ts
CHANGED
|
@@ -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;
|
package/dist/tunnel/frame.js
CHANGED
|
@@ -81,7 +81,7 @@ export class ProtocolError extends Error {
|
|
|
81
81
|
this.name = "ProtocolError";
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
/**
|
|
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
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
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)) {
|
package/dist/tunnel/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/tunnel/index.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
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",
|