@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/MIGRATION.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
Per version, newest first. Read by the platform's own version surface and served to
|
|
4
4
|
creators' agents. Accurate or absent, never speculative (B-F5-2).
|
|
5
5
|
|
|
6
|
+
## 0.21.0
|
|
7
|
+
|
|
8
|
+
required: false
|
|
9
|
+
No action required unless a build output holds a credential file. Behaviour that changed:
|
|
10
|
+
- `vincentt publish` refuses a build output that holds a file or folder named like a credential (`credentials.json`, `service-account*.json`, `*firebase-adminsdk*.json`, `*.key`, `*.p12`, `*.pfx`, `*.p8`, `*.ppk`, `id_rsa*`, `id_dsa*`, `id_ed25519*`, `id_ecdsa*`, `*.jks`, `*.keystore`, `*.env`, `*.tfstate`, `*.tfvars`, and their `~`, `.bak`, `.gz`, `.br` or `.zst` copies, in any letter case).
|
|
11
|
+
- `vincentt publish` also refuses a build output holding a file with a private key (including one a bundler inlined into JavaScript from an environment variable), or a Google service-account or user credential JSON.
|
|
12
|
+
- A file that changes or becomes a link while publish reads it is also refused.
|
|
13
|
+
- A publish that worked before can now be refused, with no override: move the file out of the build output (or rename an asset such as a Keynote `.key`) and rebuild. Public `.pem` certificates are still published.
|
|
14
|
+
- `vincentt publish` can print a new "Heavy files" warning section in the checklist (a model over 10 MB, or an image over 2048 px on its long side); exit codes and prompts are unchanged.
|
|
15
|
+
- Every request the CLI makes to the platform, including a preview's start, check-in and stop, and every preview tunnel connection, now says which CLI version is calling (`X-Vincentt-Client: cli/<version>`).
|
|
16
|
+
- A new exit code, 5: the platform refused this CLI's version and nothing was changed on the platform. `create` may already have written the project, and its screen says where. No minimum version is set by this release, so nothing is refused yet.
|
|
17
|
+
- On exit 5 the screen names this CLI's version, the minimum it needs and how to upgrade; `publish --json` prints `{"state":"failed","exitCode":5}`; the newer-version notice is not printed after it. When the platform received no version at all, the screen asks for a report instead of an upgrade.
|
|
18
|
+
- `vincentt preview` has two new endings: the CLI is too old (exit 5), and "Vincentt stopped confirming it" when the platform stops confirming a running preview (exit 1, and no new preview is started on its own). The "Couldn't confirm your access to this project." ending is gone.
|
|
19
|
+
- A request body sent to a preview with no declared length (chunked, or HTTP/2 without Content-Length) now reaches the dev server whole, sent with `Transfer-Encoding: chunked`, and a request with no body completes at once. Against a platform edge that predates this release the previous behaviour stays. Each forwarded request uses its own connection to the dev server, and a request whose Content-Length disagrees with its body is reset on its own without ending the preview.
|
|
20
|
+
|
|
6
21
|
## 0.20.0
|
|
7
22
|
|
|
8
23
|
required: false
|
package/dist/cli/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { fileLocationStore } from "../shared/locationStore.js";
|
|
|
24
24
|
import { getMe, listProjects, ProjectUnreachableError } from "../lib/backend.js";
|
|
25
25
|
import { EXIT } from "../lib/exitCodes.js";
|
|
26
26
|
import { AuthExpiredError, authExpiredLines, isAuthRefused, NOT_SIGNED_IN_LINES, NotSignedInError, } from "../shared/authError.js";
|
|
27
|
+
import { UpgradeRequiredError, upgradeRefusalLines, } from "../shared/upgradeError.js";
|
|
27
28
|
import { listProjectsCommand } from "../lib/listProjects.js";
|
|
28
29
|
import { initCommand } from "../lib/initCommand.js";
|
|
29
30
|
import { linkCommand, printUnreachable, unlinkCommand } from "../lib/projects.js";
|
|
@@ -340,6 +341,7 @@ async function create() {
|
|
|
340
341
|
dir = safe;
|
|
341
342
|
}
|
|
342
343
|
const interactive = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
344
|
+
const consoleUrl = (await resolveAuthUrls()).consoleUrl;
|
|
343
345
|
const result = await projectCreate(projectDir(), {
|
|
344
346
|
name: flag("--name"),
|
|
345
347
|
slug: flag("--slug"),
|
|
@@ -354,8 +356,18 @@ async function create() {
|
|
|
354
356
|
await login();
|
|
355
357
|
},
|
|
356
358
|
orgSource,
|
|
357
|
-
consoleUrl
|
|
359
|
+
consoleUrl,
|
|
358
360
|
onLine: (l) => console.log(l),
|
|
361
|
+
}).catch((err) => {
|
|
362
|
+
// Registration is refused after the source is already on disk, so the
|
|
363
|
+
// screen says where it is.
|
|
364
|
+
if (!(err instanceof UpgradeRequiredError))
|
|
365
|
+
throw err;
|
|
366
|
+
const where = dir === undefined ? undefined : `./${dir}`;
|
|
367
|
+
for (const l of upgradeRefusalLines(err, { kind: "create", dir: where })) {
|
|
368
|
+
console.log(l);
|
|
369
|
+
}
|
|
370
|
+
return { message: "", exitCode: EXIT.UPGRADE_REQUIRED };
|
|
359
371
|
});
|
|
360
372
|
if (result.exitCode !== 0) {
|
|
361
373
|
process.exitCode = result.exitCode;
|
|
@@ -627,7 +639,18 @@ async function preview() {
|
|
|
627
639
|
}
|
|
628
640
|
}
|
|
629
641
|
let ending = null;
|
|
642
|
+
let endingRefusal;
|
|
630
643
|
let resolveDone;
|
|
644
|
+
const refusedAtStart = (facts) => {
|
|
645
|
+
for (const l of upgradeRefusalLines(facts, {
|
|
646
|
+
kind: "preview_start",
|
|
647
|
+
devCommand,
|
|
648
|
+
localUrl: handRunUrl,
|
|
649
|
+
})) {
|
|
650
|
+
console.log(l);
|
|
651
|
+
}
|
|
652
|
+
process.exitCode = EXIT.UPGRADE_REQUIRED;
|
|
653
|
+
};
|
|
631
654
|
const done = new Promise((r) => {
|
|
632
655
|
resolveDone = r;
|
|
633
656
|
});
|
|
@@ -656,8 +679,9 @@ async function preview() {
|
|
|
656
679
|
onOriginError: () => ui.originError(),
|
|
657
680
|
onOriginRecovered: () => ui.originRecovered(),
|
|
658
681
|
onExpiring: () => ui.checkHardMax(running?.expiresAt ?? ""),
|
|
659
|
-
onEnded: (reason) => {
|
|
682
|
+
onEnded: (reason, refusal) => {
|
|
660
683
|
ending = reason;
|
|
684
|
+
endingRefusal = refusal;
|
|
661
685
|
resolveDone?.();
|
|
662
686
|
},
|
|
663
687
|
});
|
|
@@ -669,6 +693,11 @@ async function preview() {
|
|
|
669
693
|
}
|
|
670
694
|
}
|
|
671
695
|
catch (err) {
|
|
696
|
+
// Refused by the mint, before the dev server was spawned: nothing to stop.
|
|
697
|
+
if (err instanceof UpgradeRequiredError) {
|
|
698
|
+
refusedAtStart(err);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
672
701
|
// The local org is the FALLBACK only. A count refusal names the org that
|
|
673
702
|
// owns this project, and the wire's name wins over this one wherever both
|
|
674
703
|
// exist — see resolveCountLimit.
|
|
@@ -678,6 +707,13 @@ async function preview() {
|
|
|
678
707
|
process.exitCode = err instanceof AuthExpiredError ? EXIT.NOT_AUTHENTICATED : 1;
|
|
679
708
|
return;
|
|
680
709
|
}
|
|
710
|
+
// A first dial refused before the start block printed: the creator never saw
|
|
711
|
+
// a link, so this is the start-time screen, not the running preview's ending.
|
|
712
|
+
if (ending === "upgrade_required") {
|
|
713
|
+
await previewStop();
|
|
714
|
+
refusedAtStart(endingRefusal ?? {});
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
681
717
|
const qr = shouldRenderQr({
|
|
682
718
|
isTTY: Boolean(process.stdout.isTTY),
|
|
683
719
|
noQr: has("--no-qr"),
|
|
@@ -714,11 +750,19 @@ async function preview() {
|
|
|
714
750
|
await done;
|
|
715
751
|
clearInterval(ceiling);
|
|
716
752
|
if (ending !== null) {
|
|
717
|
-
ui.endedBy(ending, (await activeOrgName()) || "your workspace");
|
|
753
|
+
ui.endedBy(ending, (await activeOrgName()) || "your workspace", endingRefusal);
|
|
718
754
|
await previewStop();
|
|
719
|
-
process.exitCode = ending
|
|
755
|
+
process.exitCode = previewExitCode(ending);
|
|
720
756
|
}
|
|
721
757
|
}
|
|
758
|
+
/** A preview's ending → its exit: 5 for a version refusal, 4 for a refused token, else 1. */
|
|
759
|
+
function previewExitCode(ending) {
|
|
760
|
+
if (ending === "upgrade_required")
|
|
761
|
+
return EXIT.UPGRADE_REQUIRED;
|
|
762
|
+
if (ending === "auth_expired")
|
|
763
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
764
|
+
return EXIT.FAILED;
|
|
765
|
+
}
|
|
722
766
|
/** A `--port` or `--dev-command` value the schema refuses, before anything runs. */
|
|
723
767
|
function previewFlagRefusal(port, devCommand) {
|
|
724
768
|
const portMessage = port === undefined
|
|
@@ -1530,8 +1574,12 @@ function journal(exitCode, error) {
|
|
|
1530
1574
|
// a notice is worth nothing if it costs a second on every publish.
|
|
1531
1575
|
try {
|
|
1532
1576
|
const cached = loadCache();
|
|
1533
|
-
|
|
1534
|
-
|
|
1577
|
+
// Suppressed on a version refusal: the ending already names the number that
|
|
1578
|
+
// matters, and a stale cache could name a `latest` below the floor.
|
|
1579
|
+
if (exitCode !== EXIT.UPGRADE_REQUIRED) {
|
|
1580
|
+
for (const l of updateNotice(cliVersion(), cached?.latest))
|
|
1581
|
+
console.error(l);
|
|
1582
|
+
}
|
|
1535
1583
|
if (!isFresh(cached, Date.now()))
|
|
1536
1584
|
refreshUpdateCache();
|
|
1537
1585
|
}
|
|
@@ -1623,6 +1671,15 @@ main()
|
|
|
1623
1671
|
journal(EXIT.NOT_AUTHENTICATED, err);
|
|
1624
1672
|
process.exit(EXIT.NOT_AUTHENTICATED);
|
|
1625
1673
|
}
|
|
1674
|
+
// L22's generic screen, for every verb without its own arm. Prose on the
|
|
1675
|
+
// verb's usual channel, or stderr under --json so stdout stays a document.
|
|
1676
|
+
if (err instanceof UpgradeRequiredError) {
|
|
1677
|
+
const say = has("--json") ? console.error : console.log;
|
|
1678
|
+
for (const l of upgradeRefusalLines(err))
|
|
1679
|
+
say(l);
|
|
1680
|
+
journal(EXIT.UPGRADE_REQUIRED, err);
|
|
1681
|
+
process.exit(EXIT.UPGRADE_REQUIRED);
|
|
1682
|
+
}
|
|
1626
1683
|
if (isAuthRefused(err)) {
|
|
1627
1684
|
for (const l of authExpiredLines())
|
|
1628
1685
|
console.error(l);
|
package/dist/domain/api.js
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
// ordinary indistinguishable 404 before the hostname is read at all. The CLI
|
|
13
13
|
// therefore cannot distinguish "no such project" from "not your project" — and
|
|
14
14
|
// must not try.
|
|
15
|
-
import { clientHeaders } from "../shared/clientHeader.js";
|
|
16
15
|
import { rejectAuthExpired } from "../shared/authError.js";
|
|
17
|
-
import { apiFetch } from "../shared/http.js";
|
|
16
|
+
import { apiFetch, apiRequest } from "../shared/http.js";
|
|
17
|
+
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
18
18
|
import { parseDomain, parseDomainList, } from "./wire.js";
|
|
19
19
|
export class DomainApiError extends Error {
|
|
20
20
|
status;
|
|
@@ -30,8 +30,12 @@ export class DomainApiError extends Error {
|
|
|
30
30
|
this.name = "DomainApiError";
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Rejects with UpgradeRequiredError on a 426 and AuthExpiredError on a 401;
|
|
35
|
+
* every other status becomes a DomainApiError.
|
|
36
|
+
*/
|
|
34
37
|
async function readError(res) {
|
|
38
|
+
await rejectUpgradeRequired(res);
|
|
35
39
|
rejectAuthExpired(res);
|
|
36
40
|
let body = {};
|
|
37
41
|
try {
|
|
@@ -44,8 +48,8 @@ async function readError(res) {
|
|
|
44
48
|
const retryAfter = res.headers.get("retry-after");
|
|
45
49
|
return new DomainApiError(res.status, body.code ?? `http_${res.status}`, body.message ?? `request failed (${res.status})`, { attached: num(body.attached), entitled: num(body.entitled) }, retryAfter ? Number(retryAfter) : undefined);
|
|
46
50
|
}
|
|
47
|
-
function
|
|
48
|
-
return
|
|
51
|
+
function call(cfg, path, init, fetchImpl) {
|
|
52
|
+
return apiRequest(cfg, path, init, { authenticated: true }, fetchImpl);
|
|
49
53
|
}
|
|
50
54
|
function rolesOf(raw) {
|
|
51
55
|
const o = typeof raw === "object" && raw !== null ? raw : {};
|
|
@@ -57,11 +61,11 @@ function rolesOf(raw) {
|
|
|
57
61
|
}
|
|
58
62
|
/** POST /projects/{id}/domains — returns the records to publish. Rate-limited. */
|
|
59
63
|
export async function attachDomain(cfg, projectId, domain, fetchImpl = apiFetch) {
|
|
60
|
-
const res = await
|
|
64
|
+
const res = await call(cfg, `/projects/${encodeURIComponent(projectId)}/domains`, {
|
|
61
65
|
method: "POST",
|
|
62
|
-
headers: {
|
|
66
|
+
headers: { "content-type": "application/json" },
|
|
63
67
|
body: JSON.stringify({ domain }),
|
|
64
|
-
});
|
|
68
|
+
}, fetchImpl);
|
|
65
69
|
if (!res.ok)
|
|
66
70
|
throw await readError(res);
|
|
67
71
|
const raw = await res.json();
|
|
@@ -82,7 +86,7 @@ export async function attachDomain(cfg, projectId, domain, fetchImpl = apiFetch)
|
|
|
82
86
|
* a defect, and its copy names no limit family.
|
|
83
87
|
*/
|
|
84
88
|
export async function verifyDomain(cfg, projectId, domain, fetchImpl = apiFetch) {
|
|
85
|
-
const res = await
|
|
89
|
+
const res = await call(cfg, `/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}/verify`, { method: "POST" }, fetchImpl);
|
|
86
90
|
if (!res.ok)
|
|
87
91
|
throw await readError(res);
|
|
88
92
|
const raw = await res.json();
|
|
@@ -98,7 +102,7 @@ export async function verifyDomain(cfg, projectId, domain, fetchImpl = apiFetch)
|
|
|
98
102
|
}
|
|
99
103
|
/** GET /projects/{id}/domains — this project's domains with state + findings. */
|
|
100
104
|
export async function listProjectDomains(cfg, projectId, fetchImpl = apiFetch) {
|
|
101
|
-
const res = await
|
|
105
|
+
const res = await call(cfg, `/projects/${encodeURIComponent(projectId)}/domains`, {}, fetchImpl);
|
|
102
106
|
if (!res.ok)
|
|
103
107
|
throw await readError(res);
|
|
104
108
|
return parseDomainList(await res.json());
|
|
@@ -112,14 +116,14 @@ export async function listProjectDomains(cfg, projectId, fetchImpl = apiFetch) {
|
|
|
112
116
|
* here, and this route only ever returns their own.
|
|
113
117
|
*/
|
|
114
118
|
export async function listOrgDomains(cfg, orgSlug, fetchImpl = apiFetch) {
|
|
115
|
-
const res = await
|
|
119
|
+
const res = await call(cfg, `/orgs/${encodeURIComponent(orgSlug)}/domains`, {}, fetchImpl);
|
|
116
120
|
if (!res.ok)
|
|
117
121
|
throw await readError(res);
|
|
118
122
|
return parseDomainList(await res.json());
|
|
119
123
|
}
|
|
120
124
|
/** DELETE /projects/{id}/domains/{domain} — detach. The row is never deleted server-side. */
|
|
121
125
|
export async function detachDomain(cfg, projectId, domain, fetchImpl = apiFetch) {
|
|
122
|
-
const res = await
|
|
126
|
+
const res = await call(cfg, `/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`, { method: "DELETE" }, fetchImpl);
|
|
123
127
|
// 404 is a SILENT RESOLVE on detach: the creator asked for a state the world is
|
|
124
128
|
// already in. It is not distinguishable from "not your project", and it does
|
|
125
129
|
// not need to be — either way this project is not serving that name.
|
package/dist/domain/command.js
CHANGED
|
@@ -22,6 +22,7 @@ import { EXIT } from "../lib/exitCodes.js";
|
|
|
22
22
|
import { VALIDATION_RECORD_GONE } from "./wire.js";
|
|
23
23
|
import { DomainApiError } from "./api.js";
|
|
24
24
|
import { AuthExpiredError, authExpiredLines } from "../shared/authError.js";
|
|
25
|
+
import { UpgradeRequiredError } from "../shared/upgradeError.js";
|
|
25
26
|
import { localRefusal, normalizeArgument } from "./hostname.js";
|
|
26
27
|
import * as copy from "./copy.js";
|
|
27
28
|
/** The canonical subcommands. */
|
|
@@ -178,6 +179,8 @@ function authExpired(deps) {
|
|
|
178
179
|
return EXIT.NOT_AUTHENTICATED;
|
|
179
180
|
}
|
|
180
181
|
function printAttachError(deps, err, hostname) {
|
|
182
|
+
if (err instanceof UpgradeRequiredError)
|
|
183
|
+
throw err;
|
|
181
184
|
if (err instanceof AuthExpiredError)
|
|
182
185
|
return authExpired(deps);
|
|
183
186
|
if (!(err instanceof DomainApiError)) {
|
|
@@ -239,6 +242,8 @@ async function listVerb(deps, cfg, opts) {
|
|
|
239
242
|
view = await deps.listOrg(cfg, org.org.slug);
|
|
240
243
|
}
|
|
241
244
|
catch (err) {
|
|
245
|
+
if (err instanceof UpgradeRequiredError)
|
|
246
|
+
throw err;
|
|
242
247
|
if (err instanceof AuthExpiredError)
|
|
243
248
|
return authExpired(deps);
|
|
244
249
|
deps.print("");
|
|
@@ -293,6 +298,8 @@ async function verifyVerb(deps, cfg, opts) {
|
|
|
293
298
|
result = await deps.verify(cfg, binding.projectId, hostname);
|
|
294
299
|
}
|
|
295
300
|
catch (err) {
|
|
301
|
+
if (err instanceof UpgradeRequiredError)
|
|
302
|
+
throw err;
|
|
296
303
|
if (err instanceof AuthExpiredError)
|
|
297
304
|
return authExpired(deps);
|
|
298
305
|
if (err instanceof DomainApiError && err.status === 404) {
|
|
@@ -459,6 +466,8 @@ async function detachVerb(deps, cfg, opts) {
|
|
|
459
466
|
await deps.detach(cfg, binding.projectId, hostname);
|
|
460
467
|
}
|
|
461
468
|
catch (err) {
|
|
469
|
+
if (err instanceof UpgradeRequiredError)
|
|
470
|
+
throw err;
|
|
462
471
|
if (err instanceof AuthExpiredError)
|
|
463
472
|
return authExpired(deps);
|
|
464
473
|
deps.print("");
|
package/dist/dx/command.js
CHANGED
|
@@ -4,6 +4,7 @@ import { journalEnabled, journalPath, readJournal } from "./journal.js";
|
|
|
4
4
|
import { buildReport, renderPayload } from "./send.js";
|
|
5
5
|
import { resolveBuiltWith } from "../platform/resolve.js";
|
|
6
6
|
import { EXIT } from "../lib/exitCodes.js";
|
|
7
|
+
import { UpgradeRequiredError } from "../shared/upgradeError.js";
|
|
7
8
|
const MINUTE_MS = 60 * 1000;
|
|
8
9
|
const HOUR_MS = 60 * MINUTE_MS;
|
|
9
10
|
const DAY_MS = 24 * HOUR_MS;
|
|
@@ -526,6 +527,8 @@ export async function dxSendCommand(deps, options) {
|
|
|
526
527
|
return 0;
|
|
527
528
|
}
|
|
528
529
|
catch (err) {
|
|
530
|
+
if (err instanceof UpgradeRequiredError)
|
|
531
|
+
throw err;
|
|
529
532
|
deps.print("");
|
|
530
533
|
deps.print(`✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
531
534
|
return 1;
|
package/dist/dx/consent.js
CHANGED
|
@@ -22,8 +22,8 @@ import { browserPage } from "../login/pkceLogin.js";
|
|
|
22
22
|
import { stateMatches } from "../login/pkce.js";
|
|
23
23
|
import { getFreePort } from "../preview/net.js";
|
|
24
24
|
import { authFailureMessage } from "../shared/authError.js";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { apiRequest } from "../shared/http.js";
|
|
26
|
+
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
27
27
|
import { readAccountConsent } from "./enablement.js";
|
|
28
28
|
async function safeText(res) {
|
|
29
29
|
try {
|
|
@@ -48,32 +48,28 @@ function unwrapMe(raw) {
|
|
|
48
48
|
}
|
|
49
49
|
/** The real HTTP-backed ConsentBackend, pointed at a Vincentt API base URL. */
|
|
50
50
|
export function httpConsentBackend() {
|
|
51
|
-
const
|
|
51
|
+
const call = async (apiUrl, pat, path, init) => {
|
|
52
|
+
const res = await apiRequest({ apiUrl, pat }, path, init, { authenticated: true });
|
|
53
|
+
await rejectUpgradeRequired(res);
|
|
54
|
+
return res;
|
|
55
|
+
};
|
|
52
56
|
return {
|
|
53
57
|
async read(apiUrl, pat, now) {
|
|
54
|
-
const res = await
|
|
55
|
-
headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
|
|
56
|
-
});
|
|
58
|
+
const res = await call(apiUrl, pat, "/me", {});
|
|
57
59
|
if (!res.ok) {
|
|
58
60
|
throw new Error(authFailureMessage("Could not read your account", res.status, await safeText(res)));
|
|
59
61
|
}
|
|
60
62
|
return readAccountConsent(unwrapMe(await res.json()), now);
|
|
61
63
|
},
|
|
62
64
|
async grant(apiUrl, pat, now) {
|
|
63
|
-
const res = await
|
|
64
|
-
method: "POST",
|
|
65
|
-
headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
|
|
66
|
-
});
|
|
65
|
+
const res = await call(apiUrl, pat, "/me/dx-consent", { method: "POST" });
|
|
67
66
|
if (!res.ok) {
|
|
68
67
|
throw new Error(authFailureMessage("Could not record consent", res.status, await safeText(res)));
|
|
69
68
|
}
|
|
70
69
|
return readAccountConsent(unwrapMe(await res.json()), now);
|
|
71
70
|
},
|
|
72
71
|
async revoke(apiUrl, pat) {
|
|
73
|
-
const res = await
|
|
74
|
-
method: "DELETE",
|
|
75
|
-
headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
|
|
76
|
-
});
|
|
72
|
+
const res = await call(apiUrl, pat, "/me/dx-consent", { method: "DELETE" });
|
|
77
73
|
if (!res.ok) {
|
|
78
74
|
throw new Error(authFailureMessage("Could not revoke consent", res.status, await safeText(res)));
|
|
79
75
|
}
|
package/dist/dx/reportIo.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { DXSendError } from "./send.js";
|
|
2
2
|
import { AUTH_EXPIRED_MESSAGE } from "../shared/authError.js";
|
|
3
|
-
import { apiFetch } from "../shared/http.js";
|
|
3
|
+
import { apiFetch, apiRequest } from "../shared/http.js";
|
|
4
|
+
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
4
5
|
export async function postObservationReport(apiUrl, pat, report, fetchImpl = apiFetch) {
|
|
5
|
-
const res = await
|
|
6
|
+
const res = await apiRequest({ apiUrl, pat }, "/dx-reports", {
|
|
6
7
|
method: "POST",
|
|
7
|
-
headers: { "content-type": "application/json"
|
|
8
|
+
headers: { "content-type": "application/json" },
|
|
8
9
|
body: JSON.stringify(report),
|
|
9
|
-
});
|
|
10
|
+
}, { authenticated: true }, fetchImpl);
|
|
10
11
|
if (!res.ok) {
|
|
12
|
+
await rejectUpgradeRequired(res);
|
|
11
13
|
let detail = "";
|
|
12
14
|
try {
|
|
13
15
|
const body = (await res.json());
|
package/dist/dx/send.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AUTH_EXPIRED_MESSAGE } from "../shared/authError.js";
|
|
2
|
-
import { apiFetch } from "../shared/http.js";
|
|
2
|
+
import { apiFetch, apiRequest } from "../shared/http.js";
|
|
3
|
+
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
3
4
|
import { renderPayloadWithVersionSummary, summarizeBuiltWith, versionDisclosureLines, } from "../platform/versionSummary.js";
|
|
4
5
|
/**
|
|
5
6
|
* The server bounds a report at 50 findings and refuses the whole thing past
|
|
@@ -105,12 +106,13 @@ export class DXSendError extends Error {
|
|
|
105
106
|
* POST the report. The caller has already shown the payload and had it accepted.
|
|
106
107
|
*/
|
|
107
108
|
export async function postReport(apiUrl, pat, report, fetchImpl = apiFetch) {
|
|
108
|
-
const res = await
|
|
109
|
+
const res = await apiRequest({ apiUrl, pat }, "/dx-reports", {
|
|
109
110
|
method: "POST",
|
|
110
|
-
headers: { "content-type": "application/json"
|
|
111
|
+
headers: { "content-type": "application/json" },
|
|
111
112
|
body: JSON.stringify(report),
|
|
112
|
-
});
|
|
113
|
+
}, { authenticated: true }, fetchImpl);
|
|
113
114
|
if (!res.ok) {
|
|
115
|
+
await rejectUpgradeRequired(res);
|
|
114
116
|
let detail = "";
|
|
115
117
|
try {
|
|
116
118
|
const body = (await res.json());
|
package/dist/lib/backend.js
CHANGED
|
@@ -6,9 +6,9 @@ import { promises as fs } from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
-
import { clientHeaders } from "../shared/clientHeader.js";
|
|
10
9
|
import { authFailureMessage } from "../shared/authError.js";
|
|
11
|
-
import {
|
|
10
|
+
import { apiRequest } from "../shared/http.js";
|
|
11
|
+
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
export class BackendError extends Error {
|
|
14
14
|
status;
|
|
@@ -19,14 +19,9 @@ export class BackendError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
async function authFetch(cfg, pathname, init = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
...(init.headers ?? {}),
|
|
26
|
-
...clientHeaders(),
|
|
27
|
-
authorization: `Bearer ${cfg.pat}`,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
22
|
+
const res = await apiRequest(cfg, pathname, init, { authenticated: true });
|
|
23
|
+
await rejectUpgradeRequired(res);
|
|
24
|
+
return res;
|
|
30
25
|
}
|
|
31
26
|
async function safeText(res) {
|
|
32
27
|
try {
|
package/dist/lib/exitCodes.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export declare const EXIT: {
|
|
|
17
17
|
readonly DELIVERED_NOT_REGISTERED: 3;
|
|
18
18
|
/** A platform call needed an account and the command did not answer for it. */
|
|
19
19
|
readonly NOT_AUTHENTICATED: 4;
|
|
20
|
+
/**
|
|
21
|
+
* The platform refused this CLI's version, and nothing was changed on the
|
|
22
|
+
* platform. A verb such as `create` may have written local source; its screen
|
|
23
|
+
* says where.
|
|
24
|
+
*/
|
|
25
|
+
readonly UPGRADE_REQUIRED: 5;
|
|
20
26
|
/** A bounded wait elapsed with nothing handed over. */
|
|
21
27
|
readonly WINDOW_ELAPSED: 64;
|
|
22
28
|
/** The channel ended before anything was handed over. */
|
package/dist/lib/exitCodes.js
CHANGED
|
@@ -17,6 +17,12 @@ export const EXIT = {
|
|
|
17
17
|
DELIVERED_NOT_REGISTERED: 3,
|
|
18
18
|
/** A platform call needed an account and the command did not answer for it. */
|
|
19
19
|
NOT_AUTHENTICATED: 4,
|
|
20
|
+
/**
|
|
21
|
+
* The platform refused this CLI's version, and nothing was changed on the
|
|
22
|
+
* platform. A verb such as `create` may have written local source; its screen
|
|
23
|
+
* says where.
|
|
24
|
+
*/
|
|
25
|
+
UPGRADE_REQUIRED: 5,
|
|
20
26
|
/** A bounded wait elapsed with nothing handed over. */
|
|
21
27
|
WINDOW_ELAPSED: 64,
|
|
22
28
|
/** The channel ended before anything was handed over. */
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export declare const CONTROL_CHARS: RegExp;
|
|
2
2
|
/** Each C0/C1 control character as a visible `\uXXXX`, so nothing is hidden or redrawn. */
|
|
3
3
|
export declare function escapeControl(text: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Each C0/C1 control, bidi embedding, override or isolate, and direction mark
|
|
6
|
+
* as a visible `\uXXXX`: a file name cannot reorder the line, and the creator
|
|
7
|
+
* can still find the file.
|
|
8
|
+
*/
|
|
9
|
+
export declare function escapeTerminal(text: string): string;
|
|
4
10
|
/** Text with every C0/C1 control and bidi override or isolate removed. */
|
|
5
11
|
export declare function terminalSafe(text: string): string;
|
package/dist/lib/terminalText.js
CHANGED
|
@@ -11,6 +11,16 @@ export function escapeControl(text) {
|
|
|
11
11
|
// U+202A..U+202E (embeddings, overrides) and U+2066..U+2069 (isolates) can
|
|
12
12
|
// reorder a terminal line, so a crafted name could disguise what it says.
|
|
13
13
|
const BIDI_CONTROLS_G = /[\u202a-\u202e\u2066-\u2069]/g;
|
|
14
|
+
// The above, plus the direction marks U+200E, U+200F and U+061C.
|
|
15
|
+
const TERMINAL_UNSAFE_G = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
|
|
16
|
+
/**
|
|
17
|
+
* Each C0/C1 control, bidi embedding, override or isolate, and direction mark
|
|
18
|
+
* as a visible `\uXXXX`: a file name cannot reorder the line, and the creator
|
|
19
|
+
* can still find the file.
|
|
20
|
+
*/
|
|
21
|
+
export function escapeTerminal(text) {
|
|
22
|
+
return text.replace(TERMINAL_UNSAFE_G, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
23
|
+
}
|
|
14
24
|
/** Text with every C0/C1 control and bidi override or isolate removed. */
|
|
15
25
|
export function terminalSafe(text) {
|
|
16
26
|
return text.replace(CONTROL_CHARS_G, "").replace(BIDI_CONTROLS_G, "");
|
package/dist/lib/whoami.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// f2's hardest copy rather than cosmetic.
|
|
8
8
|
import { resolveActiveOrg } from "./org.js";
|
|
9
9
|
import { authExpiredLines, isAuthRefused } from "../shared/authError.js";
|
|
10
|
+
import { UpgradeRequiredError } from "../shared/upgradeError.js";
|
|
10
11
|
import { EXIT } from "./exitCodes.js";
|
|
11
12
|
export async function whoamiCommand(deps) {
|
|
12
13
|
const cfg = await deps.config();
|
|
@@ -19,6 +20,8 @@ export async function whoamiCommand(deps) {
|
|
|
19
20
|
me = await deps.getMe(cfg);
|
|
20
21
|
}
|
|
21
22
|
catch (err) {
|
|
23
|
+
if (err instanceof UpgradeRequiredError)
|
|
24
|
+
throw err;
|
|
22
25
|
if (isAuthRefused(err)) {
|
|
23
26
|
for (const l of authExpiredLines())
|
|
24
27
|
deps.print(l);
|
|
@@ -18,6 +18,7 @@ import { DOWNLOAD_DIR, downloadPath, exists, overwriteRefusalLines, writeAsset,
|
|
|
18
18
|
import { dedupeForDisplay, parseIdArgs, parseSearchFlags } from "./parse.js";
|
|
19
19
|
import { addLines, humanBytes, searchLines, showLines, unknownBlockLines, unreachableLines, } from "./render.js";
|
|
20
20
|
import { isLibraryId } from "./wire.js";
|
|
21
|
+
import { UpgradeRequiredError } from "../shared/upgradeError.js";
|
|
21
22
|
/**
|
|
22
23
|
* Map a thrown client error onto the exit-4 screen it deserves.
|
|
23
24
|
*
|
|
@@ -28,6 +29,9 @@ import { isLibraryId } from "./wire.js";
|
|
|
28
29
|
* corpus failed to load.
|
|
29
30
|
*/
|
|
30
31
|
function failureScreen(err, retry) {
|
|
32
|
+
// Rethrown so main() renders the one refusal screen and exits 5.
|
|
33
|
+
if (err instanceof UpgradeRequiredError)
|
|
34
|
+
throw err;
|
|
31
35
|
if (err instanceof LibraryUnreachableError) {
|
|
32
36
|
return { lines: unreachableLines("transport", retry), code: 4 };
|
|
33
37
|
}
|
package/dist/library/client.js
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
// CLI-supplied URL reaches a fetch. That is a security control, not a style choice
|
|
13
13
|
// (D-add-prints-urls degraded the no-invented-URLs rule from prevention to
|
|
14
14
|
// detection, and this is the one arm that stayed prevention).
|
|
15
|
-
import { clientHeaders } from "../shared/clientHeader.js";
|
|
16
15
|
import { authFailureMessage } from "../shared/authError.js";
|
|
17
|
-
import {
|
|
16
|
+
import { apiRequest, withDeadline } from "../shared/http.js";
|
|
17
|
+
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
18
18
|
import { parseBatchResponse, parseFetchResponse, parseSearchResponse, } from "./wire.js";
|
|
19
19
|
// An asset file can be tens of megabytes, so its download gets longer than an api call.
|
|
20
20
|
const downloadFetch = withDeadline((input, init) => globalThis.fetch(input, init), 5 * 60_000);
|
|
@@ -65,18 +65,16 @@ async function readJson(res) {
|
|
|
65
65
|
* correct recovery differs between them (retry later vs re-search).
|
|
66
66
|
*/
|
|
67
67
|
export function httpLibraryClient(cfg) {
|
|
68
|
-
const base = cfg.apiUrl.replace(/\/$/, "");
|
|
69
|
-
const auth = { ...clientHeaders(), authorization: `Bearer ${cfg.pat}` };
|
|
70
68
|
const call = async (pathname, init) => {
|
|
69
|
+
let res;
|
|
71
70
|
try {
|
|
72
|
-
|
|
73
|
-
...init,
|
|
74
|
-
headers: { ...(init?.headers ?? {}), ...auth },
|
|
75
|
-
});
|
|
71
|
+
res = await apiRequest(cfg, pathname, init, { authenticated: true });
|
|
76
72
|
}
|
|
77
73
|
catch (err) {
|
|
78
74
|
throw new LibraryUnreachableError(err);
|
|
79
75
|
}
|
|
76
|
+
await rejectUpgradeRequired(res);
|
|
77
|
+
return res;
|
|
80
78
|
};
|
|
81
79
|
const fail = async (res, what) => {
|
|
82
80
|
// A 401 keeps its status: assetsCommand maps it to the sign-in screen and exit 4.
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// GET /me Bearer <PAT> → identity + orgs (confirm)
|
|
10
10
|
// and, to end this machine's own sign-in, DELETE /access-tokens/current Bearer <PAT>.
|
|
11
11
|
import { authFailureMessage } from "../shared/authError.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { apiRequest, withDeadline } from "../shared/http.js";
|
|
13
|
+
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
14
14
|
/** DELETE /access-tokens/current gives up after this long: sign-out never hangs on the api. */
|
|
15
15
|
export const REVOKE_CURRENT_TIMEOUT_MS = 5_000;
|
|
16
16
|
class AuthError extends Error {
|
|
@@ -52,12 +52,18 @@ function readEvicted(raw) {
|
|
|
52
52
|
}
|
|
53
53
|
/** The real HTTP-backed AuthBackend, pointed at a Vincentt API base URL. */
|
|
54
54
|
export function httpAuthBackend(apiBase) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const target = { apiUrl: apiBase };
|
|
56
|
+
// The exchange is unauthenticated but judged by the CLI floor, so it carries
|
|
57
|
+
// the tag like every other api request and its 426 is read before any arm.
|
|
58
|
+
const post = async (body) => {
|
|
59
|
+
const res = await apiRequest(target, "/access-tokens/exchange", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "content-type": "application/json" },
|
|
62
|
+
body: JSON.stringify(body),
|
|
63
|
+
}, { authenticated: false });
|
|
64
|
+
await rejectUpgradeRequired(res);
|
|
65
|
+
return res;
|
|
66
|
+
};
|
|
61
67
|
return {
|
|
62
68
|
async exchange(code, codeVerifier, label) {
|
|
63
69
|
const body = { code, code_verifier: codeVerifier };
|
|
@@ -89,9 +95,10 @@ export function httpAuthBackend(apiBase) {
|
|
|
89
95
|
};
|
|
90
96
|
},
|
|
91
97
|
async me(pat) {
|
|
92
|
-
const res = await
|
|
93
|
-
|
|
98
|
+
const res = await apiRequest({ apiUrl: apiBase, pat }, "/me", {}, {
|
|
99
|
+
authenticated: true,
|
|
94
100
|
});
|
|
101
|
+
await rejectUpgradeRequired(res);
|
|
95
102
|
if (!res.ok) {
|
|
96
103
|
throw new AuthError(authFailureMessage("Identity check failed", res.status, await safeText(res)), res.status);
|
|
97
104
|
}
|
|
@@ -106,12 +113,8 @@ export function httpAuthBackend(apiBase) {
|
|
|
106
113
|
* failure, a timeout or a 5xx is "unreachable".
|
|
107
114
|
*/
|
|
108
115
|
export async function revokeCurrent(apiBase, pat, fetchImpl = (input, init) => globalThis.fetch(input, init), timeoutMs = REVOKE_CURRENT_TIMEOUT_MS) {
|
|
109
|
-
const base = apiBase.replace(/\/$/, "");
|
|
110
116
|
try {
|
|
111
|
-
const res = await
|
|
112
|
-
method: "DELETE",
|
|
113
|
-
headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
|
|
114
|
-
});
|
|
117
|
+
const res = await apiRequest({ apiUrl: apiBase, pat }, "/access-tokens/current", { method: "DELETE" }, { authenticated: true }, withDeadline(fetchImpl, timeoutMs));
|
|
115
118
|
if (res.ok || [400, 401, 403, 404, 405].includes(res.status))
|
|
116
119
|
return "done";
|
|
117
120
|
return "unreachable";
|