@vincentt-xr/cli 0.21.0 → 0.23.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 +26 -0
- package/dist/cli/args.js +2 -0
- package/dist/cli/index.js +50 -8
- package/dist/cli/usage-text.js +10 -1
- package/dist/cli/verb-surface.d.ts +2 -1
- package/dist/cli/verb-surface.js +5 -0
- package/dist/dx/command.d.ts +74 -18
- package/dist/dx/command.js +137 -86
- package/dist/dx/consent.d.ts +25 -6
- package/dist/dx/consent.js +58 -25
- package/dist/dx/enablement.d.ts +63 -11
- package/dist/dx/enablement.js +93 -21
- package/dist/dx/journal.d.ts +3 -0
- package/dist/dx/journal.js +6 -1
- package/dist/dx/report.d.ts +4 -24
- package/dist/dx/report.js +32 -25
- package/dist/dx/skeleton.js +3 -0
- package/dist/editor/app.d.ts +61 -0
- package/dist/editor/app.js +180 -0
- package/dist/editor/copy.d.ts +22 -0
- package/dist/editor/copy.js +95 -0
- package/dist/editor/handoffs.d.ts +12 -0
- package/dist/editor/handoffs.js +93 -0
- package/dist/editor/launcher.d.ts +86 -0
- package/dist/editor/launcher.js +255 -0
- package/dist/editor/localFiles.d.ts +31 -0
- package/dist/editor/localFiles.js +100 -0
- package/dist/editor/reads.d.ts +47 -0
- package/dist/editor/reads.js +269 -0
- package/dist/editor/text.d.ts +7 -0
- package/dist/editor/text.js +58 -0
- package/dist/editor/tools.d.ts +5 -0
- package/dist/editor/tools.js +20 -0
- package/dist/lib/version.d.ts +28 -2
- package/dist/lib/version.js +30 -3
- package/dist/preview/command.js +2 -0
- package/dist/preview/copy.d.ts +5 -0
- package/dist/preview/copy.js +15 -0
- package/dist/preview/runner.d.ts +6 -0
- package/dist/preview/runner.js +1 -0
- package/dist/publish/command.js +5 -2
- package/dist/publish/copy.d.ts +6 -0
- package/dist/publish/copy.js +27 -6
- package/dist/publish/manifest.d.ts +11 -2
- package/dist/publish/manifest.js +45 -9
- package/dist/publish/upload.d.ts +10 -0
- package/dist/publish/upload.js +23 -0
- package/dist/shared/config.d.ts +6 -6
- package/dist/tunnel/client.d.ts +11 -3
- package/dist/tunnel/client.js +11 -8
- package/package.json +2 -2
package/dist/publish/copy.js
CHANGED
|
@@ -404,7 +404,7 @@ export function tooLarge(v) {
|
|
|
404
404
|
"",
|
|
405
405
|
"✗ One file is too large to publish.",
|
|
406
406
|
"",
|
|
407
|
-
...alignedPair([v.path ?? "", formatBytes(v.observed)], ["the per-file limit", formatLimit(v.limit)]),
|
|
407
|
+
...alignedPair([escapeTerminal(v.path ?? ""), formatBytes(v.observed)], ["the per-file limit", formatLimit(v.limit)]),
|
|
408
408
|
"",
|
|
409
409
|
" Compress or externalise it, rebuild, then publish again.",
|
|
410
410
|
"",
|
|
@@ -453,9 +453,10 @@ function alignedPair(a, b) {
|
|
|
453
453
|
];
|
|
454
454
|
}
|
|
455
455
|
function largestBlock(v) {
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
456
|
+
const largest = v.largest.map((f) => ({ path: escapeTerminal(f.path), size: f.size }));
|
|
457
|
+
const width = Math.max(0, ...largest.map((f) => f.path.length));
|
|
458
|
+
const sizeWidth = Math.max(0, ...largest.map((f) => formatBytes(f.size).length));
|
|
459
|
+
return largest.map((f) => ` ${f.path.padEnd(width)} ${formatBytes(f.size).padStart(sizeWidth)}`);
|
|
459
460
|
}
|
|
460
461
|
/** No dist at all, or no index.html at its root. Zero network calls. */
|
|
461
462
|
/**
|
|
@@ -532,7 +533,7 @@ export function outputOutside(shown) {
|
|
|
532
533
|
export function outputInHidden(shown, hidden) {
|
|
533
534
|
return [
|
|
534
535
|
"",
|
|
535
|
-
`✗ The build output ${escapeTerminal(shown)} is inside ${hidden}/, not a build.`,
|
|
536
|
+
`✗ The build output ${escapeTerminal(shown)} is inside ${escapeTerminal(hidden)}/, not a build.`,
|
|
536
537
|
"",
|
|
537
538
|
" Publishing it would put your repository's history or its Vincentt link at a",
|
|
538
539
|
" public address. Set --dist, or build.output in vincentt.json, to the folder",
|
|
@@ -585,6 +586,26 @@ export function secretFiles(paths, output = DEFAULT_OUTPUT) {
|
|
|
585
586
|
"",
|
|
586
587
|
];
|
|
587
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Flow U1 — files whose folder resolved outside the output after the walk: a
|
|
591
|
+
* folder was swapped for a link while publish read it. The whole publish is
|
|
592
|
+
* refused, never just the file. After the header and reading line.
|
|
593
|
+
*/
|
|
594
|
+
export function escapedFiles(paths, output = DEFAULT_OUTPUT) {
|
|
595
|
+
return [
|
|
596
|
+
"",
|
|
597
|
+
`✗ These files resolved outside ${escapeTerminal(output.folder)} while publish was reading them.`,
|
|
598
|
+
"",
|
|
599
|
+
...offenderList(paths, output.folder),
|
|
600
|
+
"",
|
|
601
|
+
" A folder in the build output became a link to somewhere else, so publish",
|
|
602
|
+
" cannot vouch for what it would upload. Let your build finish, then publish",
|
|
603
|
+
" again.",
|
|
604
|
+
"",
|
|
605
|
+
" Nothing was uploaded.",
|
|
606
|
+
"",
|
|
607
|
+
];
|
|
608
|
+
}
|
|
588
609
|
/** Flow U1 — credential files, from the early checks or the hash step. After the header and reading line. */
|
|
589
610
|
export function credentialFiles(paths, output = DEFAULT_OUTPUT) {
|
|
590
611
|
return [
|
|
@@ -914,7 +935,7 @@ export function publishIncomplete(address, counts, liveVersion) {
|
|
|
914
935
|
export function badDigest(filePath, address, liveVersion) {
|
|
915
936
|
return [
|
|
916
937
|
"",
|
|
917
|
-
`✗ ${filePath} changed while uploading. Rebuild and publish again.`,
|
|
938
|
+
`✗ ${escapeTerminal(filePath)} changed while uploading. Rebuild and publish again.`,
|
|
918
939
|
"",
|
|
919
940
|
...unchangedLines(address, liveVersion),
|
|
920
941
|
"",
|
|
@@ -11,7 +11,7 @@ export interface Manifest {
|
|
|
11
11
|
files: ManifestEntry[];
|
|
12
12
|
totalBytes: number;
|
|
13
13
|
}
|
|
14
|
-
export type ManifestReason = PathRejection | "no_dist" | "output_is_repo_root" | "output_outside_repo" | "output_in_hidden" | "secret_path" | "credential_file" | "lfs_pointer" | "file_unreadable";
|
|
14
|
+
export type ManifestReason = PathRejection | "no_dist" | "output_is_repo_root" | "output_outside_repo" | "output_in_hidden" | "secret_path" | "path_escaped" | "credential_file" | "lfs_pointer" | "file_unreadable";
|
|
15
15
|
export type CredentialWhy = "name" | "private_key" | CredentialJsonType;
|
|
16
16
|
/** An output-relative path the leak, credential or LFS check refused. */
|
|
17
17
|
export interface Offender {
|
|
@@ -64,6 +64,15 @@ export interface BuildManifestOptions {
|
|
|
64
64
|
* change a checked file. Never set by the command.
|
|
65
65
|
*/
|
|
66
66
|
beforeHash?: (files: readonly WalkedFile[]) => Promise<void> | void;
|
|
67
|
+
/**
|
|
68
|
+
* Test-only: runs before the walk reads each folder, so a test can swap a
|
|
69
|
+
* folder mid-walk. Never set by the command.
|
|
70
|
+
*/
|
|
71
|
+
beforeReadDir?: (dir: string) => Promise<void> | void;
|
|
72
|
+
}
|
|
73
|
+
/** Test-only hooks into the checks. */
|
|
74
|
+
export interface CheckOutputHooks {
|
|
75
|
+
beforeReadDir?: (dir: string) => Promise<void> | void;
|
|
67
76
|
}
|
|
68
77
|
/** A Git LFS pointer file: at most 1 KB, the spec's exact first line, and an oid line. */
|
|
69
78
|
export declare function isLfsPointer(head: Uint8Array, size: number): boolean;
|
|
@@ -77,7 +86,7 @@ export declare function readHead(file: WalkedFile): Promise<Buffer>;
|
|
|
77
86
|
* or sent. Returns the file list the checks passed, which is exactly the list
|
|
78
87
|
* the manifest is built from: what is checked is what is uploaded.
|
|
79
88
|
*/
|
|
80
|
-
export declare function checkOutput(distDir: string, repoRoot: string): Promise<WalkedFile[]>;
|
|
89
|
+
export declare function checkOutput(distDir: string, repoRoot: string, hooks?: CheckOutputHooks): Promise<WalkedFile[]>;
|
|
81
90
|
export interface HashResult {
|
|
82
91
|
/** base64 std, the exact form `x-amz-checksum-sha256` wants. */
|
|
83
92
|
sha256: string;
|
package/dist/publish/manifest.js
CHANGED
|
@@ -55,15 +55,23 @@ function isJsonName(rel) {
|
|
|
55
55
|
* symlink is resolved and judged against the real repo root, and a name on the
|
|
56
56
|
* credential list is recorded apart (a folder once, never descended).
|
|
57
57
|
*/
|
|
58
|
-
async function walkOutput(outReal, rootReal) {
|
|
58
|
+
async function walkOutput(outReal, rootReal, hooks) {
|
|
59
59
|
const walk = { files: [], offenders: [], credentialNames: [] };
|
|
60
60
|
let seq = 0;
|
|
61
|
+
const relOf = (full) => path.relative(outReal, full).split(path.sep).join("/");
|
|
62
|
+
// A read error's message carries the raw absolute path. It becomes a refusal
|
|
63
|
+
// naming the output-relative path, which the copy escapes.
|
|
64
|
+
const unreadable = (rel) => new ManifestError("file_unreadable", rel, "a path could not be read");
|
|
61
65
|
const visit = async (dir) => {
|
|
62
|
-
|
|
66
|
+
await hooks.beforeReadDir?.(dir);
|
|
67
|
+
const dirRel = dir === outReal ? "" : `${relOf(dir)}/`;
|
|
68
|
+
const entries = await fs
|
|
69
|
+
.readdir(dir, { withFileTypes: true })
|
|
70
|
+
.catch(() => Promise.reject(unreadable(dirRel)));
|
|
63
71
|
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
64
72
|
for (const entry of entries) {
|
|
65
73
|
const full = path.join(dir, entry.name);
|
|
66
|
-
const rel =
|
|
74
|
+
const rel = relOf(full);
|
|
67
75
|
const atRoot = dir === outReal;
|
|
68
76
|
const lower = entry.name.toLowerCase();
|
|
69
77
|
if (entry.name.startsWith(".")) {
|
|
@@ -101,7 +109,7 @@ async function walkOutput(outReal, rootReal) {
|
|
|
101
109
|
await visit(full);
|
|
102
110
|
}
|
|
103
111
|
else if (entry.isFile()) {
|
|
104
|
-
const stat = await fs.lstat(full);
|
|
112
|
+
const stat = await fs.lstat(full).catch(() => Promise.reject(unreadable(rel)));
|
|
105
113
|
const at = seq++;
|
|
106
114
|
if (nameMatched) {
|
|
107
115
|
walk.credentialNames.push({ seq: at, offender: { path: rel, why: "name" } });
|
|
@@ -121,6 +129,25 @@ async function walkOutput(outReal, rootReal) {
|
|
|
121
129
|
await visit(outReal);
|
|
122
130
|
return walk;
|
|
123
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Files whose real parent folder is not inside the real output. The walk reads
|
|
134
|
+
* a folder by path, so a folder swapped for a symlink mid-walk is walked through
|
|
135
|
+
* the link; this catches it after the fact. A parent that cannot be resolved
|
|
136
|
+
* counts as escaped: it is never assumed inside.
|
|
137
|
+
*/
|
|
138
|
+
async function escapedFiles(files, outReal) {
|
|
139
|
+
const parents = new Map();
|
|
140
|
+
const inside = (dir) => {
|
|
141
|
+
let p = parents.get(dir);
|
|
142
|
+
if (p === undefined) {
|
|
143
|
+
p = fs.realpath(dir).then((real) => real === outReal || isInside(real, outReal), () => false);
|
|
144
|
+
parents.set(dir, p);
|
|
145
|
+
}
|
|
146
|
+
return p;
|
|
147
|
+
};
|
|
148
|
+
const verdicts = await mapLimit(files, READ_CONCURRENCY, (f) => inside(path.dirname(f.abs)));
|
|
149
|
+
return files.filter((_, i) => !verdicts[i]).map((f) => ({ path: f.rel }));
|
|
150
|
+
}
|
|
124
151
|
/** Where a symlink lands: its realpath, or its lexical target when it dangles. */
|
|
125
152
|
async function symlinkTarget(link) {
|
|
126
153
|
try {
|
|
@@ -235,7 +262,7 @@ function forbiddenSegmentIn(rel) {
|
|
|
235
262
|
* or sent. Returns the file list the checks passed, which is exactly the list
|
|
236
263
|
* the manifest is built from: what is checked is what is uploaded.
|
|
237
264
|
*/
|
|
238
|
-
export async function checkOutput(distDir, repoRoot) {
|
|
265
|
+
export async function checkOutput(distDir, repoRoot, hooks = {}) {
|
|
239
266
|
const rootReal = await fs.realpath(repoRoot).catch(() => path.resolve(repoRoot));
|
|
240
267
|
const lexical = path.resolve(distDir);
|
|
241
268
|
const lexicalRoot = path.resolve(repoRoot);
|
|
@@ -253,7 +280,7 @@ export async function checkOutput(distDir, repoRoot) {
|
|
|
253
280
|
if (err.code === "ENOENT") {
|
|
254
281
|
throw new ManifestError("no_dist", distDir, `no dist at ${distDir}`);
|
|
255
282
|
}
|
|
256
|
-
throw
|
|
283
|
+
throw new ManifestError("file_unreadable", "", "the output could not be read");
|
|
257
284
|
}
|
|
258
285
|
if (outReal === rootReal) {
|
|
259
286
|
throw new ManifestError("output_is_repo_root", distDir, `${distDir} resolves to the repo root`);
|
|
@@ -266,14 +293,21 @@ export async function checkOutput(distDir, repoRoot) {
|
|
|
266
293
|
if (hiddenParent !== undefined) {
|
|
267
294
|
throw new ManifestError("output_in_hidden", hiddenParent, `${distDir} is inside ${hiddenParent}`);
|
|
268
295
|
}
|
|
269
|
-
|
|
296
|
+
const outStat = await fs.stat(outReal).catch(() => {
|
|
297
|
+
throw new ManifestError("file_unreadable", "", "the output could not be read");
|
|
298
|
+
});
|
|
299
|
+
if (!outStat.isDirectory()) {
|
|
270
300
|
throw new ManifestError("no_dist", distDir, `no dist at ${distDir}`);
|
|
271
301
|
}
|
|
272
|
-
const walk = await walkOutput(outReal, rootReal);
|
|
302
|
+
const walk = await walkOutput(outReal, rootReal, hooks);
|
|
273
303
|
const stats = {
|
|
274
304
|
files: walk.files.length,
|
|
275
305
|
bytes: walk.files.reduce((n, f) => n + f.size, 0),
|
|
276
306
|
};
|
|
307
|
+
const escaped = await escapedFiles(walk.files, outReal);
|
|
308
|
+
if (escaped.length > 0) {
|
|
309
|
+
throw new ManifestError("path_escaped", escaped[0].path, "files resolved outside the output", { paths: escaped, stats });
|
|
310
|
+
}
|
|
277
311
|
if (walk.offenders.length > 0) {
|
|
278
312
|
throw new ManifestError("secret_path", walk.offenders[0].path, "hidden paths in the output", {
|
|
279
313
|
paths: walk.offenders,
|
|
@@ -385,7 +419,9 @@ export async function hashAndScan(absPath, walked) {
|
|
|
385
419
|
export async function buildManifest(distDir, opts) {
|
|
386
420
|
// The list the checks passed IS the upload list. A second walk could see a
|
|
387
421
|
// file created after the checks ran.
|
|
388
|
-
const walked = await checkOutput(distDir, opts.repoRoot
|
|
422
|
+
const walked = await checkOutput(distDir, opts.repoRoot, {
|
|
423
|
+
beforeReadDir: opts.beforeReadDir,
|
|
424
|
+
});
|
|
389
425
|
const normalized = [];
|
|
390
426
|
for (const file of walked) {
|
|
391
427
|
const result = normalizePath(file.rel);
|
package/dist/publish/upload.d.ts
CHANGED
|
@@ -30,6 +30,16 @@ export declare class UploadTargetRejected extends Error {
|
|
|
30
30
|
readonly path: string;
|
|
31
31
|
constructor(path: string);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A target's signed checksum is not the one this run's manifest holds for that
|
|
35
|
+
* path. The PUT reopens the file by path, so the signed checksum is what makes
|
|
36
|
+
* "checked is uploaded" hold; a target that would sign anything else is refused
|
|
37
|
+
* before any byte is sent.
|
|
38
|
+
*/
|
|
39
|
+
export declare class UploadChecksumMismatch extends Error {
|
|
40
|
+
readonly path: string;
|
|
41
|
+
constructor(path: string);
|
|
42
|
+
}
|
|
33
43
|
export interface UploadProgress {
|
|
34
44
|
filesDone: number;
|
|
35
45
|
filesTotal: number;
|
package/dist/publish/upload.js
CHANGED
|
@@ -58,6 +58,25 @@ export class UploadTargetRejected extends Error {
|
|
|
58
58
|
this.name = "UploadTargetRejected";
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* A target's signed checksum is not the one this run's manifest holds for that
|
|
63
|
+
* path. The PUT reopens the file by path, so the signed checksum is what makes
|
|
64
|
+
* "checked is uploaded" hold; a target that would sign anything else is refused
|
|
65
|
+
* before any byte is sent.
|
|
66
|
+
*/
|
|
67
|
+
export class UploadChecksumMismatch extends Error {
|
|
68
|
+
path;
|
|
69
|
+
constructor(path) {
|
|
70
|
+
super(`the upload target ${path} does not carry this build's checksum`);
|
|
71
|
+
this.path = path;
|
|
72
|
+
this.name = "UploadChecksumMismatch";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const CHECKSUM_HEADER = "x-amz-checksum-sha256";
|
|
76
|
+
function signedChecksum(headers) {
|
|
77
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === CHECKSUM_HEADER);
|
|
78
|
+
return key === undefined ? undefined : headers[key];
|
|
79
|
+
}
|
|
61
80
|
const DEFAULT_CONCURRENCY = 6;
|
|
62
81
|
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
63
82
|
const BACKOFF_BASE_MS = 500;
|
|
@@ -114,6 +133,10 @@ export async function uploadAll(opts) {
|
|
|
114
133
|
const foreign = opts.targets.find((t) => !sizes.has(t.path));
|
|
115
134
|
if (foreign !== undefined)
|
|
116
135
|
throw new UploadTargetRejected(foreign.path);
|
|
136
|
+
const sums = new Map(opts.manifest.map((f) => [f.path, f.sha256]));
|
|
137
|
+
const mismatched = opts.targets.find((t) => signedChecksum(t.headers) !== sums.get(t.path));
|
|
138
|
+
if (mismatched !== undefined)
|
|
139
|
+
throw new UploadChecksumMismatch(mismatched.path);
|
|
117
140
|
const bytesTotal = opts.targets.reduce((n, t) => n + (sizes.get(t.path) ?? 0), 0);
|
|
118
141
|
const queue = [...opts.targets];
|
|
119
142
|
const retry = {
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -8,14 +8,14 @@ export interface MachineConfig {
|
|
|
8
8
|
/** Org slug commands act in when no --org flag or VINCENTT_ORG env is given. */
|
|
9
9
|
activeOrg: string;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* `vincentt dx enable
|
|
11
|
+
* This machine's copy of the account's unattended-send grant. Written by
|
|
12
|
+
* `vincentt dx enable` only after the account grant succeeded, on either path.
|
|
13
|
+
* Authorizes a send on its own ONLY when GET /me cannot be read. Cleared by
|
|
14
|
+
* `disable`, by `logout`, and by any /me answer that is not a grant.
|
|
13
15
|
*/
|
|
14
16
|
dxSendEnabled?: boolean;
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
* no expiry, so an unparseable value fails closed rather than printing.
|
|
18
|
-
*/
|
|
17
|
+
/** ISO8601 UTC: the account's dxReportsConsentAt on the terminal path; the CLI's
|
|
18
|
+
* clock on the browser path, whose callback carries no date. */
|
|
19
19
|
dxSendEnabledAt?: string;
|
|
20
20
|
}
|
|
21
21
|
/**
|
package/dist/tunnel/client.d.ts
CHANGED
|
@@ -157,6 +157,13 @@ export interface TunnelClientOptions {
|
|
|
157
157
|
* this preview, 410 the session is gone. They map to different creator actions.
|
|
158
158
|
*/
|
|
159
159
|
readonly onReconnectRefused?: (status: number) => void;
|
|
160
|
+
/**
|
|
161
|
+
* The edge broke the agreed wire: a malformed 101 caps header, a CONTROL
|
|
162
|
+
* frame this connection cannot read, or a fatal OPEN. Only an edge bug causes
|
|
163
|
+
* it, and a redial
|
|
164
|
+
* reaches the same edge, so the client makes no further dial after this.
|
|
165
|
+
*/
|
|
166
|
+
readonly onProtocolViolation?: () => void;
|
|
160
167
|
/** Injectable for tests; never a real socket in a unit test. */
|
|
161
168
|
readonly connect?: (url: string, headers: Record<string, string>) => WebSocket;
|
|
162
169
|
}
|
|
@@ -232,10 +239,11 @@ export declare class TunnelClient {
|
|
|
232
239
|
private open;
|
|
233
240
|
private onClosed;
|
|
234
241
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
242
|
+
* End the preview for a violation the edge alone could cause. A redial reaches
|
|
243
|
+
* the same edge and gets the same answer: before this was terminal, a
|
|
244
|
+
* persistently malformed 101 redialled every 8 seconds indefinitely.
|
|
237
245
|
*/
|
|
238
|
-
private
|
|
246
|
+
private fatalViolation;
|
|
239
247
|
private startPing;
|
|
240
248
|
private stopPing;
|
|
241
249
|
private send;
|
package/dist/tunnel/client.js
CHANGED
|
@@ -281,10 +281,10 @@ export class TunnelClient {
|
|
|
281
281
|
// The agreed set fixes this connection's grammar. Absent is the base
|
|
282
282
|
// protocol (an edge that predates negotiation); a malformed answer, or
|
|
283
283
|
// one naming a token this client did not offer, is the edge
|
|
284
|
-
// misbehaving and
|
|
284
|
+
// misbehaving and ends the preview.
|
|
285
285
|
const agreed = parseAgreedCaps(res.headers[CAPS_HEADER], res.rawHeaders);
|
|
286
286
|
if (agreed === null) {
|
|
287
|
-
this.
|
|
287
|
+
this.fatalViolation(ws);
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
290
|
this.caps = agreed;
|
|
@@ -382,12 +382,15 @@ export class TunnelClient {
|
|
|
382
382
|
}, delay);
|
|
383
383
|
}
|
|
384
384
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
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.
|
|
387
388
|
*/
|
|
388
|
-
|
|
389
|
-
if (this.ws !== ws)
|
|
389
|
+
fatalViolation(ws) {
|
|
390
|
+
if (ws === null || this.ws !== ws || this.closed)
|
|
390
391
|
return;
|
|
392
|
+
this.closed = true;
|
|
393
|
+
this.opts.onProtocolViolation?.();
|
|
391
394
|
ws.close();
|
|
392
395
|
}
|
|
393
396
|
startPing() {
|
|
@@ -453,7 +456,7 @@ export class TunnelClient {
|
|
|
453
456
|
catch {
|
|
454
457
|
// A CONTROL this connection cannot read (malformed, unknown, or a
|
|
455
458
|
// kind it did not agree) is fatal: only the edge writes stream 0.
|
|
456
|
-
this.ws
|
|
459
|
+
this.fatalViolation(this.ws);
|
|
457
460
|
return;
|
|
458
461
|
}
|
|
459
462
|
if (ctrl.kind === "viewer_gone") {
|
|
@@ -478,7 +481,7 @@ export class TunnelClient {
|
|
|
478
481
|
}
|
|
479
482
|
catch (err) {
|
|
480
483
|
if (err instanceof ProtocolError && err.fatal) {
|
|
481
|
-
this.ws
|
|
484
|
+
this.fatalViolation(this.ws);
|
|
482
485
|
return;
|
|
483
486
|
}
|
|
484
487
|
this.send(FrameType.RESET, streamId, new Uint8Array([ResetReason.PROTOCOL_ERROR]));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincentt-xr/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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,7 +28,7 @@
|
|
|
28
28
|
"qrcode": "^1.5.4",
|
|
29
29
|
"ws": "^8.18.0",
|
|
30
30
|
"zod": "^3.23.8",
|
|
31
|
-
"@vincentt-xr/harness": "3.
|
|
31
|
+
"@vincentt-xr/harness": "3.1.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/qrcode": "^1.5.5",
|