@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.
- package/MIGRATION.md +30 -0
- package/dist/cli/index.js +86 -12
- package/dist/domain/api.js +16 -12
- package/dist/domain/command.js +9 -0
- package/dist/dx/command.d.ts +74 -18
- package/dist/dx/command.js +140 -86
- package/dist/dx/consent.d.ts +25 -6
- package/dist/dx/consent.js +64 -35
- package/dist/dx/enablement.d.ts +63 -11
- package/dist/dx/enablement.js +93 -21
- package/dist/dx/report.d.ts +4 -24
- package/dist/dx/report.js +32 -25
- 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/version.d.ts +28 -2
- package/dist/lib/version.js +30 -3
- 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 +10 -5
- package/dist/preview/copy.d.ts +13 -1
- package/dist/preview/copy.js +31 -3
- package/dist/preview/proxy.js +5 -39
- package/dist/preview/runner.d.ts +31 -9
- package/dist/preview/runner.js +43 -15
- package/dist/publish/api.js +14 -14
- package/dist/publish/command.d.ts +3 -0
- package/dist/publish/command.js +74 -7
- package/dist/publish/copy.d.ts +15 -0
- package/dist/publish/copy.js +109 -19
- package/dist/publish/keyScan.d.ts +25 -0
- package/dist/publish/keyScan.js +88 -0
- package/dist/publish/manifest.d.ts +60 -19
- package/dist/publish/manifest.js +290 -71
- package/dist/publish/upload.d.ts +18 -0
- package/dist/publish/upload.js +38 -0
- package/dist/publish/weight.d.ts +23 -0
- package/dist/publish/weight.js +113 -0
- package/dist/shared/config.d.ts +6 -6
- 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 +56 -2
- package/dist/tunnel/client.js +160 -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/publish/copy.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// NEVER PRINTED, at any point, in any state: the project id, the org id, the
|
|
13
13
|
// uploadId, the PAT, any presigned URL or fragment of one, the bucket, any S3
|
|
14
14
|
// key, or a per-file upload log.
|
|
15
|
-
import {
|
|
15
|
+
import { escapeTerminal } from "../lib/terminalText.js";
|
|
16
16
|
/** The only horizontal rules in any f4 output. They exist here and nowhere else. */
|
|
17
17
|
const RULE = " ─────────────────────────────────────────────────────────────────────";
|
|
18
18
|
const LABELS = {
|
|
@@ -166,11 +166,33 @@ export function pathsFromRootLines(findings, folder = "dist/") {
|
|
|
166
166
|
out.push("", " Vincentt serves this app at the root of its address. A missing path that", " starts with / usually means the build was made for a subpath, such as a", " GitHub Pages project site. Nothing here blocks you.", "");
|
|
167
167
|
return out;
|
|
168
168
|
}
|
|
169
|
+
/** The `Heavy files` section: warning-only, absent when there are no findings. */
|
|
170
|
+
export function heavyFileLines(findings) {
|
|
171
|
+
if (!findings || findings.length === 0)
|
|
172
|
+
return [];
|
|
173
|
+
const out = [" Heavy files (each one downloads to the phone before it can show)", ""];
|
|
174
|
+
for (const f of findings.slice(0, FINDINGS_SHOWN)) {
|
|
175
|
+
const name = escapeTerminal(f.path);
|
|
176
|
+
if (f.kind === "model") {
|
|
177
|
+
out.push(` ! ${name} is ${formatBytes(f.bytes)}`);
|
|
178
|
+
out.push(`${" ".repeat(21)}a model over 10 MB`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
out.push(` ! ${name} is ${f.width} x ${f.height} px`);
|
|
182
|
+
out.push(`${" ".repeat(21)}an image over 2048 px on its long side`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (findings.length > FINDINGS_SHOWN) {
|
|
186
|
+
out.push(` and ${findings.length - FINDINGS_SHOWN} more`);
|
|
187
|
+
}
|
|
188
|
+
out.push("", " Every phone that opens this app downloads these files over its own", " connection. Compress the model or shrink the image, then build again.", " Nothing here blocks you.", "");
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
169
191
|
const DEFAULT_OUTPUT = { folder: "dist/", sourceNote: "" };
|
|
170
192
|
export function readingLine(fileCount, totalBytes, output = DEFAULT_OUTPUT) {
|
|
171
193
|
const note = output.sourceNote ? ` ${output.sourceNote}` : "";
|
|
172
194
|
// The folder is the creator's build.output or --dist, so it is escaped.
|
|
173
|
-
return `→ Reading ${
|
|
195
|
+
return `→ Reading ${escapeTerminal(output.folder)}${note} … ${fileCount} files · ${formatBytes(totalBytes)}`;
|
|
174
196
|
}
|
|
175
197
|
export function headerLine(projectName, orgName) {
|
|
176
198
|
return orgName ? `→ ${projectName} · ${orgName}` : `→ ${projectName}`;
|
|
@@ -211,6 +233,7 @@ export function firstPublishChecklist(i) {
|
|
|
211
233
|
...findingLines(i.findings, true, i.output?.folder),
|
|
212
234
|
...nothingBlocksYou(),
|
|
213
235
|
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
236
|
+
...heavyFileLines(i.heavy),
|
|
214
237
|
` Publishing v${i.nextVersion} · ${i.fileCount} files · ${formatBytes(i.totalBytes)}`,
|
|
215
238
|
"",
|
|
216
239
|
];
|
|
@@ -255,6 +278,7 @@ export function republishChecklist(i) {
|
|
|
255
278
|
"",
|
|
256
279
|
...findingLines(i.findings, false, i.output?.folder),
|
|
257
280
|
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
281
|
+
...heavyFileLines(i.heavy),
|
|
258
282
|
];
|
|
259
283
|
}
|
|
260
284
|
/** `[Y/n]` — on a republish an accidental Enter does the thing the creator typed the command to do. */
|
|
@@ -279,6 +303,7 @@ export function offlineChecklist(i) {
|
|
|
279
303
|
"",
|
|
280
304
|
...findingLines(i.findings, false, i.output?.folder),
|
|
281
305
|
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
306
|
+
...heavyFileLines(i.heavy),
|
|
282
307
|
];
|
|
283
308
|
}
|
|
284
309
|
export function offlinePrompt(version) {
|
|
@@ -303,6 +328,7 @@ export function notServingChecklist(i, stateKnown) {
|
|
|
303
328
|
"",
|
|
304
329
|
...findingLines(i.findings, false, i.output?.folder),
|
|
305
330
|
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
331
|
+
...heavyFileLines(i.heavy),
|
|
306
332
|
];
|
|
307
333
|
}
|
|
308
334
|
/** `[y/N]`: the CLI cannot say what this publish changes, so Enter does nothing. */
|
|
@@ -378,7 +404,7 @@ export function tooLarge(v) {
|
|
|
378
404
|
"",
|
|
379
405
|
"✗ One file is too large to publish.",
|
|
380
406
|
"",
|
|
381
|
-
...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)]),
|
|
382
408
|
"",
|
|
383
409
|
" Compress or externalise it, rebuild, then publish again.",
|
|
384
410
|
"",
|
|
@@ -427,9 +453,10 @@ function alignedPair(a, b) {
|
|
|
427
453
|
];
|
|
428
454
|
}
|
|
429
455
|
function largestBlock(v) {
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
|
|
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)}`);
|
|
433
460
|
}
|
|
434
461
|
/** No dist at all, or no index.html at its root. Zero network calls. */
|
|
435
462
|
/**
|
|
@@ -455,7 +482,7 @@ export function noDist(distDir, buildCommand) {
|
|
|
455
482
|
"",
|
|
456
483
|
" Nothing was uploaded.",
|
|
457
484
|
"",
|
|
458
|
-
].map(
|
|
485
|
+
].map(escapeTerminal);
|
|
459
486
|
}
|
|
460
487
|
export function noIndexHtml(distDir, buildCommand) {
|
|
461
488
|
return [
|
|
@@ -469,7 +496,7 @@ export function noIndexHtml(distDir, buildCommand) {
|
|
|
469
496
|
"",
|
|
470
497
|
" Nothing was uploaded.",
|
|
471
498
|
"",
|
|
472
|
-
].map(
|
|
499
|
+
].map(escapeTerminal);
|
|
473
500
|
}
|
|
474
501
|
/** Flow U1 — the output is the project folder itself. */
|
|
475
502
|
export function outputIsRoot(o) {
|
|
@@ -487,13 +514,13 @@ export function outputIsRoot(o) {
|
|
|
487
514
|
"",
|
|
488
515
|
" Nothing was uploaded.",
|
|
489
516
|
"",
|
|
490
|
-
].map(
|
|
517
|
+
].map(escapeTerminal);
|
|
491
518
|
}
|
|
492
519
|
/** Flow U1 — the output lies outside the project. `shown` is project-relative. */
|
|
493
520
|
export function outputOutside(shown) {
|
|
494
521
|
return [
|
|
495
522
|
"",
|
|
496
|
-
`✗ The build output ${
|
|
523
|
+
`✗ The build output ${escapeTerminal(shown)} is outside this project.`,
|
|
497
524
|
"",
|
|
498
525
|
" Publish uploads a folder inside the project. Set --dist, or build.output in",
|
|
499
526
|
" vincentt.json, to the folder your build writes.",
|
|
@@ -506,7 +533,7 @@ export function outputOutside(shown) {
|
|
|
506
533
|
export function outputInHidden(shown, hidden) {
|
|
507
534
|
return [
|
|
508
535
|
"",
|
|
509
|
-
`✗ The build output ${
|
|
536
|
+
`✗ The build output ${escapeTerminal(shown)} is inside ${escapeTerminal(hidden)}/, not a build.`,
|
|
510
537
|
"",
|
|
511
538
|
" Publishing it would put your repository's history or its Vincentt link at a",
|
|
512
539
|
" public address. Set --dist, or build.output in vincentt.json, to the folder",
|
|
@@ -516,18 +543,30 @@ export function outputInHidden(shown, hidden) {
|
|
|
516
543
|
"",
|
|
517
544
|
];
|
|
518
545
|
}
|
|
546
|
+
/** Where a credential reason starts, unless a longer path pushes it right. */
|
|
547
|
+
const WHY_COLUMN = 40;
|
|
548
|
+
const CREDENTIAL_WHY = {
|
|
549
|
+
name: "named like a credential",
|
|
550
|
+
private_key: "holds a private key",
|
|
551
|
+
service_account: "holds a service-account key",
|
|
552
|
+
authorized_user: "holds a Google sign-in credential",
|
|
553
|
+
};
|
|
519
554
|
function offenderList(paths, folder) {
|
|
520
555
|
// File names and the folder prefix come from the build and the creator's
|
|
521
556
|
// settings, and may carry control characters.
|
|
522
|
-
folder =
|
|
557
|
+
folder = escapeTerminal(folder);
|
|
523
558
|
const shown = paths.slice(0, FINDINGS_SHOWN).map((p) => ({
|
|
524
|
-
path:
|
|
525
|
-
target: p.target === undefined ? undefined :
|
|
559
|
+
path: escapeTerminal(p.path),
|
|
560
|
+
target: p.target === undefined ? undefined : escapeTerminal(p.target),
|
|
561
|
+
why: p.why,
|
|
526
562
|
}));
|
|
527
563
|
const width = Math.max(...shown.map((p) => folder.length + p.path.length));
|
|
564
|
+
const whyColumn = Math.max(WHY_COLUMN, width + 3);
|
|
528
565
|
const out = shown.map((p) => p.target !== undefined
|
|
529
566
|
? ` ${(folder + p.path).padEnd(width)} links to ${p.target}`
|
|
530
|
-
:
|
|
567
|
+
: p.why !== undefined
|
|
568
|
+
? ` ${(folder + p.path).padEnd(whyColumn)}${CREDENTIAL_WHY[p.why]}`
|
|
569
|
+
: ` ${folder}${p.path}`);
|
|
531
570
|
if (paths.length > FINDINGS_SHOWN)
|
|
532
571
|
out.push(` and ${paths.length - FINDINGS_SHOWN} more`);
|
|
533
572
|
return out;
|
|
@@ -540,13 +579,64 @@ export function secretFiles(paths, output = DEFAULT_OUTPUT) {
|
|
|
540
579
|
"",
|
|
541
580
|
...offenderList(paths, output.folder),
|
|
542
581
|
"",
|
|
543
|
-
` Everything in ${
|
|
582
|
+
` Everything in ${escapeTerminal(output.folder)} becomes public at your address. Remove these from the build`,
|
|
544
583
|
" output, rebuild, then publish.",
|
|
545
584
|
"",
|
|
546
585
|
" Nothing was uploaded.",
|
|
547
586
|
"",
|
|
548
587
|
];
|
|
549
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
|
+
}
|
|
609
|
+
/** Flow U1 — credential files, from the early checks or the hash step. After the header and reading line. */
|
|
610
|
+
export function credentialFiles(paths, output = DEFAULT_OUTPUT) {
|
|
611
|
+
return [
|
|
612
|
+
"",
|
|
613
|
+
"✗ This build holds files that look like private keys.",
|
|
614
|
+
"",
|
|
615
|
+
...offenderList(paths, output.folder),
|
|
616
|
+
"",
|
|
617
|
+
` Everything in ${escapeTerminal(output.folder)} becomes public at your address, and a published version`,
|
|
618
|
+
" cannot be taken back. Move these out of the build output, rebuild, then",
|
|
619
|
+
" publish. A file only named like a key, such as a Keynote .key, needs a new",
|
|
620
|
+
" name.",
|
|
621
|
+
"",
|
|
622
|
+
" Nothing was uploaded.",
|
|
623
|
+
"",
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
/** Flow U1 — a file that changed, became a link, or could not be read while checked or hashed. */
|
|
627
|
+
export function fileUnreadable(filePath, output = DEFAULT_OUTPUT) {
|
|
628
|
+
return [
|
|
629
|
+
"",
|
|
630
|
+
`✗ ${escapeTerminal(output.folder + filePath)} changed while publish was reading it.`,
|
|
631
|
+
"",
|
|
632
|
+
" It became a link or a different file, or could not be read, so publish",
|
|
633
|
+
" cannot vouch for what it would upload. Let your build finish, then publish",
|
|
634
|
+
" again.",
|
|
635
|
+
"",
|
|
636
|
+
" Nothing was uploaded.",
|
|
637
|
+
"",
|
|
638
|
+
];
|
|
639
|
+
}
|
|
550
640
|
/** Flow U1 — Git LFS pointer files. The recorded build.command, verbatim, when there is one. */
|
|
551
641
|
export function lfsPointers(paths, output = DEFAULT_OUTPUT, buildCommand) {
|
|
552
642
|
return [
|
|
@@ -560,7 +650,7 @@ export function lfsPointers(paths, output = DEFAULT_OUTPUT, buildCommand) {
|
|
|
560
650
|
"",
|
|
561
651
|
" git lfs pull",
|
|
562
652
|
buildCommand !== undefined
|
|
563
|
-
? ` ${
|
|
653
|
+
? ` ${escapeTerminal(buildCommand)}`
|
|
564
654
|
: " then run your build again",
|
|
565
655
|
"",
|
|
566
656
|
" Nothing was uploaded.",
|
|
@@ -581,7 +671,7 @@ export function badPath(reason, offending) {
|
|
|
581
671
|
"",
|
|
582
672
|
`✗ This build ${why[reason] ?? "has a path we can't publish"}.`,
|
|
583
673
|
"",
|
|
584
|
-
` ${
|
|
674
|
+
` ${escapeTerminal(offending)}`,
|
|
585
675
|
"",
|
|
586
676
|
" Rename it in your source, rebuild, then publish again.",
|
|
587
677
|
"",
|
|
@@ -845,7 +935,7 @@ export function publishIncomplete(address, counts, liveVersion) {
|
|
|
845
935
|
export function badDigest(filePath, address, liveVersion) {
|
|
846
936
|
return [
|
|
847
937
|
"",
|
|
848
|
-
`✗ ${filePath} changed while uploading. Rebuild and publish again.`,
|
|
938
|
+
`✗ ${escapeTerminal(filePath)} changed while uploading. Rebuild and publish again.`,
|
|
849
939
|
"",
|
|
850
940
|
...unchangedLines(address, liveVersion),
|
|
851
941
|
"",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The label is bounded to 40 characters so the carried tail stays finite. */
|
|
2
|
+
export declare const KEY_HEADER: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* A newline (real, or the two-character `\n` or four-character `\r\n` of a
|
|
5
|
+
* string literal), then a base64 line.
|
|
6
|
+
*/
|
|
7
|
+
export declare const KEY_BODY: RegExp;
|
|
8
|
+
/** Below this offset a header alone is a match. */
|
|
9
|
+
export declare const HEAD_BYTES = 4096;
|
|
10
|
+
/** How far past a header its key body may sit. */
|
|
11
|
+
export declare const BODY_WINDOW = 256;
|
|
12
|
+
export interface KeyScanner {
|
|
13
|
+
/** Feed the next chunk, as latin1. */
|
|
14
|
+
push(chunk: string): void;
|
|
15
|
+
/** Judge any header still waiting on its body window. True when a key was seen. */
|
|
16
|
+
end(): boolean;
|
|
17
|
+
readonly matched: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function createKeyScanner(): KeyScanner;
|
|
20
|
+
export type CredentialJsonType = "service_account" | "authorized_user";
|
|
21
|
+
/**
|
|
22
|
+
* The top-level `type` of a Google credential JSON, or undefined. A parse
|
|
23
|
+
* failure is "not a match" and its message is discarded: it echoes the input.
|
|
24
|
+
*/
|
|
25
|
+
export declare function credentialJson(bytes: Uint8Array): CredentialJsonType | undefined;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Content rules for credential files: a private-key header (PEM, OpenSSH, PGP)
|
|
2
|
+
// and a Google service-account or user credential JSON. Bytes are read as
|
|
3
|
+
// latin1, one character per byte, so the ASCII header matches inside UTF-8 and
|
|
4
|
+
// no decode can throw.
|
|
5
|
+
/** The label is bounded to 40 characters so the carried tail stays finite. */
|
|
6
|
+
export const KEY_HEADER = /-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY( BLOCK)?-----/;
|
|
7
|
+
/**
|
|
8
|
+
* A newline (real, or the two-character `\n` or four-character `\r\n` of a
|
|
9
|
+
* string literal), then a base64 line.
|
|
10
|
+
*/
|
|
11
|
+
export const KEY_BODY = /(?:\r?\n|(?:\\r)?\\n)[A-Za-z0-9+/=]{64}/;
|
|
12
|
+
/** Below this offset a header alone is a match. */
|
|
13
|
+
export const HEAD_BYTES = 4096;
|
|
14
|
+
/** How far past a header its key body may sit. */
|
|
15
|
+
export const BODY_WINDOW = 256;
|
|
16
|
+
/** Covers the longest header plus the body window. */
|
|
17
|
+
const CARRY = 512;
|
|
18
|
+
export function createKeyScanner() {
|
|
19
|
+
let buf = "";
|
|
20
|
+
let bufStart = 0;
|
|
21
|
+
// Absolute offset below which every header has been judged.
|
|
22
|
+
let judgedTo = 0;
|
|
23
|
+
let matched = false;
|
|
24
|
+
const scan = (final) => {
|
|
25
|
+
const re = new RegExp(KEY_HEADER.source, "g");
|
|
26
|
+
re.lastIndex = Math.max(0, judgedTo - bufStart);
|
|
27
|
+
let m;
|
|
28
|
+
while ((m = re.exec(buf)) !== null) {
|
|
29
|
+
const at = bufStart + m.index;
|
|
30
|
+
if (at < HEAD_BYTES) {
|
|
31
|
+
matched = true;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const end = m.index + m[0].length;
|
|
35
|
+
if (!final && end + BODY_WINDOW > buf.length) {
|
|
36
|
+
judgedTo = at;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (KEY_BODY.test(buf.slice(end, end + BODY_WINDOW))) {
|
|
40
|
+
matched = true;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
judgedTo = at + 1;
|
|
44
|
+
re.lastIndex = m.index + 1;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
push(chunk) {
|
|
49
|
+
if (matched)
|
|
50
|
+
return;
|
|
51
|
+
buf += chunk;
|
|
52
|
+
scan(false);
|
|
53
|
+
if (buf.length > CARRY) {
|
|
54
|
+
const drop = buf.length - CARRY;
|
|
55
|
+
buf = buf.slice(drop);
|
|
56
|
+
bufStart += drop;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
end() {
|
|
60
|
+
if (!matched)
|
|
61
|
+
scan(true);
|
|
62
|
+
return matched;
|
|
63
|
+
},
|
|
64
|
+
get matched() {
|
|
65
|
+
return matched;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The top-level `type` of a Google credential JSON, or undefined. A parse
|
|
71
|
+
* failure is "not a match" and its message is discarded: it echoes the input.
|
|
72
|
+
*/
|
|
73
|
+
export function credentialJson(bytes) {
|
|
74
|
+
const start = bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf
|
|
75
|
+
? 3
|
|
76
|
+
: 0;
|
|
77
|
+
let value;
|
|
78
|
+
try {
|
|
79
|
+
value = JSON.parse(Buffer.from(bytes.buffer, bytes.byteOffset + start, bytes.length - start).toString("utf8"));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
85
|
+
return undefined;
|
|
86
|
+
const type = value.type;
|
|
87
|
+
return type === "service_account" || type === "authorized_user" ? type : undefined;
|
|
88
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CredentialJsonType } from "./keyScan.js";
|
|
1
2
|
import { type PathRejection } from "./paths.js";
|
|
2
3
|
export interface ManifestEntry {
|
|
3
4
|
/** dist-relative, forward slashes, no leading `/`, normalized. */
|
|
@@ -10,13 +11,16 @@ export interface Manifest {
|
|
|
10
11
|
files: ManifestEntry[];
|
|
11
12
|
totalBytes: number;
|
|
12
13
|
}
|
|
13
|
-
export type ManifestReason = PathRejection | "no_dist" | "output_is_repo_root" | "output_outside_repo" | "output_in_hidden" | "secret_path" | "lfs_pointer";
|
|
14
|
-
|
|
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
|
+
export type CredentialWhy = "name" | "private_key" | CredentialJsonType;
|
|
16
|
+
/** An output-relative path the leak, credential or LFS check refused. */
|
|
15
17
|
export interface Offender {
|
|
16
18
|
/** Output-relative, POSIX. A refused folder carries a trailing `/`. */
|
|
17
19
|
path: string;
|
|
18
20
|
/** For a symlink: where it resolves, repo-relative inside and absolute outside. */
|
|
19
21
|
target?: string;
|
|
22
|
+
/** For a credential file: what made it one. */
|
|
23
|
+
why?: CredentialWhy;
|
|
20
24
|
}
|
|
21
25
|
export interface ManifestErrorDetail {
|
|
22
26
|
paths?: Offender[];
|
|
@@ -36,33 +40,70 @@ export declare class ManifestError extends Error {
|
|
|
36
40
|
};
|
|
37
41
|
constructor(reason: ManifestReason, offendingPath: string, message: string, detail?: ManifestErrorDetail);
|
|
38
42
|
}
|
|
43
|
+
/** A file the walk passed: what the hash step must find again at the same path. */
|
|
44
|
+
export interface WalkedFile {
|
|
45
|
+
/** Output-relative, POSIX. */
|
|
46
|
+
rel: string;
|
|
47
|
+
/** Absolute, under the real output folder. */
|
|
48
|
+
abs: string;
|
|
49
|
+
size: number;
|
|
50
|
+
dev: number;
|
|
51
|
+
ino: number;
|
|
52
|
+
}
|
|
39
53
|
export interface BuildManifestOptions {
|
|
40
54
|
/**
|
|
41
|
-
* The directory holding the binding.
|
|
42
|
-
* any hashing if it is the repo root,
|
|
43
|
-
* outside the two allowed ones, holds a
|
|
44
|
-
* or onto a dotted path,
|
|
55
|
+
* The directory holding the binding. Required: there is no unchecked
|
|
56
|
+
* manifest. The output is refused before any hashing if it is the repo root,
|
|
57
|
+
* lies outside it, holds a dotted name outside the two allowed ones, holds a
|
|
58
|
+
* symlink that resolves outside the root or onto a dotted path, holds a
|
|
59
|
+
* credential file, or holds a Git LFS pointer. All of it is local.
|
|
60
|
+
*/
|
|
61
|
+
repoRoot: string;
|
|
62
|
+
/**
|
|
63
|
+
* Test-only: runs after the checks and before the hash step, so a test can
|
|
64
|
+
* change a checked file. Never set by the command.
|
|
65
|
+
*/
|
|
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.
|
|
45
70
|
*/
|
|
46
|
-
|
|
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;
|
|
47
76
|
}
|
|
48
77
|
/** A Git LFS pointer file: at most 1 KB, the spec's exact first line, and an oid line. */
|
|
49
|
-
export declare function isLfsPointer(
|
|
78
|
+
export declare function isLfsPointer(head: Uint8Array, size: number): boolean;
|
|
50
79
|
/**
|
|
51
|
-
* The
|
|
52
|
-
*
|
|
53
|
-
* list the manifest is built from: what is checked is what is uploaded.
|
|
80
|
+
* The early pass's read: the first `min(size, 4096)` bytes, or the whole file
|
|
81
|
+
* for a `.json` of at most 64 KB. Opens without following links.
|
|
54
82
|
*/
|
|
55
|
-
export declare function
|
|
56
|
-
|
|
83
|
+
export declare function readHead(file: WalkedFile): Promise<Buffer>;
|
|
84
|
+
/**
|
|
85
|
+
* The leak, credential and LFS checks, in that order, before anything is hashed
|
|
86
|
+
* or sent. Returns the file list the checks passed, which is exactly the list
|
|
87
|
+
* the manifest is built from: what is checked is what is uploaded.
|
|
88
|
+
*/
|
|
89
|
+
export declare function checkOutput(distDir: string, repoRoot: string, hooks?: CheckOutputHooks): Promise<WalkedFile[]>;
|
|
90
|
+
export interface HashResult {
|
|
91
|
+
/** base64 std, the exact form `x-amz-checksum-sha256` wants. */
|
|
92
|
+
sha256: string;
|
|
57
93
|
size: number;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
|
|
94
|
+
/** Set when the hashed bytes hold a private key or a Google credential. */
|
|
95
|
+
why?: CredentialWhy;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Hash a walked file and re-run the content rules over the exact bytes hashed.
|
|
99
|
+
* The file is opened without following links and must still be the regular
|
|
100
|
+
* file the walk saw. Every read error throws `file_unreadable`; only a JSON
|
|
101
|
+
* parse failure is "not a match".
|
|
102
|
+
*/
|
|
103
|
+
export declare function hashAndScan(absPath: string, walked: WalkedFile): Promise<HashResult>;
|
|
63
104
|
/**
|
|
64
105
|
* Build the manifest for a dist directory. Every path is normalized before it is
|
|
65
106
|
* hashed, so the string this returns is the string the API validates, signs, and
|
|
66
107
|
* later lists — there is no second form anywhere.
|
|
67
108
|
*/
|
|
68
|
-
export declare function buildManifest(distDir: string, opts
|
|
109
|
+
export declare function buildManifest(distDir: string, opts: BuildManifestOptions): Promise<Manifest>;
|