@vincentt-xr/cli 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +25 -0
- package/README.md +3 -2
- package/dist/cli/index.js +95 -13
- package/dist/cli/usage-text.js +3 -2
- 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.d.ts +27 -2
- package/dist/login/authBackend.js +86 -17
- package/dist/login/loginCommand.d.ts +35 -3
- package/dist/login/loginCommand.js +81 -5
- package/dist/login/machineLabel.d.ts +46 -0
- package/dist/login/machineLabel.js +119 -0
- package/dist/login/pkceLogin.d.ts +17 -1
- package/dist/login/pkceLogin.js +30 -5
- 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 +17 -4
- 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/authError.d.ts +1 -1
- package/dist/shared/authError.js +3 -1
- 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,31 @@
|
|
|
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
|
+
|
|
21
|
+
## 0.20.0
|
|
22
|
+
|
|
23
|
+
required: false
|
|
24
|
+
No action required. Behaviour that changed:
|
|
25
|
+
- `vincentt login` signs in this machine on its own and leaves your other machines signed in. It sends a machine label, `"<OS> <arch>, <d Mon>"` by default (never the hostname), or the name given with `--label "<name>"`; a `--label` the platform would refuse (empty, over 64 characters, control or invisible characters, or a platform-reserved name) exits 1 before any browser opens.
|
|
26
|
+
- The `vincentt login` success line names the machine: `✓ Signed in as <email> on this machine (<label>)`. Against an api older than machine sign-ins the line is unchanged.
|
|
27
|
+
- When a login leaves more than 10 signed-in machines, the least recently used are signed out and each is named on its own line after the workspace line: `Signed out "<label>", the least recently used of your 10 signed-in machines (last used <d Mon>).`
|
|
28
|
+
- `vincentt logout` also ends this machine's sign-in on the platform (best-effort, 5 seconds), then always removes the local token; when the api cannot be reached it says the machine stays listed in the console. `vincentt login --force` keeps the old token until the new one is saved, then ends the old sign-in.
|
|
29
|
+
- A refused token now reads ``✗ This machine is no longer signed in to Vincentt. Run `vincentt login` to sign in again.`` (it read "Your Vincentt access token is no longer valid."). A script matching the old wording exactly needs updating. The exit code is unchanged (4).
|
|
30
|
+
|
|
6
31
|
## 0.19.0
|
|
7
32
|
|
|
8
33
|
required: false
|
package/README.md
CHANGED
|
@@ -53,8 +53,9 @@ registers the project, which is what needs an account.
|
|
|
53
53
|
|
|
54
54
|
```
|
|
55
55
|
Auth
|
|
56
|
-
login Sign in; save an access token to ~/.vincentt/config.json
|
|
57
|
-
|
|
56
|
+
login Sign in this machine; save an access token to ~/.vincentt/config.json
|
|
57
|
+
[--label <name>] names this machine (default: OS, arch, date)
|
|
58
|
+
logout Sign this machine out and remove the saved access token
|
|
58
59
|
whoami Show the signed-in account
|
|
59
60
|
|
|
60
61
|
Project
|
package/dist/cli/index.js
CHANGED
|
@@ -15,14 +15,16 @@ import { createRequire } from "node:module";
|
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { runPkceLogin } from "../login/pkceLogin.js";
|
|
18
|
-
import { httpAuthBackend } from "../login/authBackend.js";
|
|
19
|
-
import {
|
|
18
|
+
import { httpAuthBackend, revokeCurrent } from "../login/authBackend.js";
|
|
19
|
+
import { resolveLoginLabel } from "../login/machineLabel.js";
|
|
20
|
+
import { loginCommand, logoutLines as logoutCommand } from "../login/loginCommand.js";
|
|
20
21
|
import { ensureLoggedIn } from "../login/ensure.js";
|
|
21
22
|
import { clearMachineToken, loadMachineConfig, locateProjectBinding, machineApiUrl, resolveConfig, writeDxEnablement, MACHINE_CONFIG_PATH, } from "../shared/config.js";
|
|
22
23
|
import { fileLocationStore } from "../shared/locationStore.js";
|
|
23
24
|
import { getMe, listProjects, ProjectUnreachableError } from "../lib/backend.js";
|
|
24
25
|
import { EXIT } from "../lib/exitCodes.js";
|
|
25
26
|
import { AuthExpiredError, authExpiredLines, isAuthRefused, NOT_SIGNED_IN_LINES, NotSignedInError, } from "../shared/authError.js";
|
|
27
|
+
import { UpgradeRequiredError, upgradeRefusalLines, } from "../shared/upgradeError.js";
|
|
26
28
|
import { listProjectsCommand } from "../lib/listProjects.js";
|
|
27
29
|
import { initCommand } from "../lib/initCommand.js";
|
|
28
30
|
import { linkCommand, printUnreachable, unlinkCommand } from "../lib/projects.js";
|
|
@@ -120,16 +122,33 @@ function machineCredentials(machine) {
|
|
|
120
122
|
return pat ? { apiUrl: machineApiUrl(machine), pat } : undefined;
|
|
121
123
|
}
|
|
122
124
|
/** The PKCE loopback + code-exchange login (f1). The raw PAT never rides a redirect query. */
|
|
123
|
-
async function runLoginFlow() {
|
|
125
|
+
async function runLoginFlow(label) {
|
|
124
126
|
const { consoleUrl, apiUrl } = await resolveAuthUrls();
|
|
125
127
|
return runPkceLogin({
|
|
126
128
|
consoleUrl,
|
|
127
129
|
apiUrl,
|
|
130
|
+
label,
|
|
128
131
|
onProgress: (line) => console.log(line),
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
|
-
/**
|
|
134
|
+
/** The label this login sends, or undefined when --label was refused (the refusal is printed). */
|
|
135
|
+
function loginLabel() {
|
|
136
|
+
const eq = valueOfEqualsFlag(argv, "--label");
|
|
137
|
+
const resolved = resolveLoginLabel(has("--label") || eq !== undefined, flag("--label") ?? eq);
|
|
138
|
+
if (!resolved.ok) {
|
|
139
|
+
for (const line of resolved.lines)
|
|
140
|
+
console.log(line);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
return resolved.label;
|
|
144
|
+
}
|
|
145
|
+
/** `vincentt login` [--force] [--label <name>] — the L1/L2/L4/L5 transcripts. */
|
|
132
146
|
async function login() {
|
|
147
|
+
const label = loginLabel();
|
|
148
|
+
if (label === undefined) {
|
|
149
|
+
process.exitCode = EXIT.FAILED;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
133
152
|
const { apiUrl } = await resolveAuthUrls();
|
|
134
153
|
const code = await loginCommand({
|
|
135
154
|
print: (line) => console.log(line),
|
|
@@ -140,10 +159,12 @@ async function login() {
|
|
|
140
159
|
throw new NotSignedInError();
|
|
141
160
|
return httpAuthBackend(apiUrl).me(pat);
|
|
142
161
|
},
|
|
143
|
-
runLogin: runLoginFlow,
|
|
162
|
+
runLogin: () => runLoginFlow(label),
|
|
144
163
|
clearToken: async () => {
|
|
145
164
|
await clearMachineToken();
|
|
146
165
|
},
|
|
166
|
+
storedToken: async () => (await loadMachineConfig()).pat,
|
|
167
|
+
revokeToken: (pat) => revokeCurrent(apiUrl, pat),
|
|
147
168
|
home: os.homedir(),
|
|
148
169
|
configPath: MACHINE_CONFIG_PATH,
|
|
149
170
|
}, { force: has("--force") });
|
|
@@ -169,8 +190,13 @@ async function requireLogin() {
|
|
|
169
190
|
return ok;
|
|
170
191
|
}
|
|
171
192
|
async function logout() {
|
|
172
|
-
const
|
|
173
|
-
|
|
193
|
+
const machine = await loadMachineConfig();
|
|
194
|
+
const outcome = machine.pat
|
|
195
|
+
? await revokeCurrent(machineApiUrl(machine), machine.pat)
|
|
196
|
+
: "done";
|
|
197
|
+
const lines = await logoutCommand({ clear: clearMachineToken, outcome });
|
|
198
|
+
for (const line of lines)
|
|
199
|
+
console.log(line);
|
|
174
200
|
}
|
|
175
201
|
/**
|
|
176
202
|
* `whoami` reads the MACHINE config's API host, never the folder's binding — it
|
|
@@ -315,6 +341,7 @@ async function create() {
|
|
|
315
341
|
dir = safe;
|
|
316
342
|
}
|
|
317
343
|
const interactive = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
344
|
+
const consoleUrl = (await resolveAuthUrls()).consoleUrl;
|
|
318
345
|
const result = await projectCreate(projectDir(), {
|
|
319
346
|
name: flag("--name"),
|
|
320
347
|
slug: flag("--slug"),
|
|
@@ -329,8 +356,18 @@ async function create() {
|
|
|
329
356
|
await login();
|
|
330
357
|
},
|
|
331
358
|
orgSource,
|
|
332
|
-
consoleUrl
|
|
359
|
+
consoleUrl,
|
|
333
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 };
|
|
334
371
|
});
|
|
335
372
|
if (result.exitCode !== 0) {
|
|
336
373
|
process.exitCode = result.exitCode;
|
|
@@ -602,7 +639,18 @@ async function preview() {
|
|
|
602
639
|
}
|
|
603
640
|
}
|
|
604
641
|
let ending = null;
|
|
642
|
+
let endingRefusal;
|
|
605
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
|
+
};
|
|
606
654
|
const done = new Promise((r) => {
|
|
607
655
|
resolveDone = r;
|
|
608
656
|
});
|
|
@@ -631,8 +679,9 @@ async function preview() {
|
|
|
631
679
|
onOriginError: () => ui.originError(),
|
|
632
680
|
onOriginRecovered: () => ui.originRecovered(),
|
|
633
681
|
onExpiring: () => ui.checkHardMax(running?.expiresAt ?? ""),
|
|
634
|
-
onEnded: (reason) => {
|
|
682
|
+
onEnded: (reason, refusal) => {
|
|
635
683
|
ending = reason;
|
|
684
|
+
endingRefusal = refusal;
|
|
636
685
|
resolveDone?.();
|
|
637
686
|
},
|
|
638
687
|
});
|
|
@@ -644,6 +693,11 @@ async function preview() {
|
|
|
644
693
|
}
|
|
645
694
|
}
|
|
646
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
|
+
}
|
|
647
701
|
// The local org is the FALLBACK only. A count refusal names the org that
|
|
648
702
|
// owns this project, and the wire's name wins over this one wherever both
|
|
649
703
|
// exist — see resolveCountLimit.
|
|
@@ -653,6 +707,13 @@ async function preview() {
|
|
|
653
707
|
process.exitCode = err instanceof AuthExpiredError ? EXIT.NOT_AUTHENTICATED : 1;
|
|
654
708
|
return;
|
|
655
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
|
+
}
|
|
656
717
|
const qr = shouldRenderQr({
|
|
657
718
|
isTTY: Boolean(process.stdout.isTTY),
|
|
658
719
|
noQr: has("--no-qr"),
|
|
@@ -689,11 +750,19 @@ async function preview() {
|
|
|
689
750
|
await done;
|
|
690
751
|
clearInterval(ceiling);
|
|
691
752
|
if (ending !== null) {
|
|
692
|
-
ui.endedBy(ending, (await activeOrgName()) || "your workspace");
|
|
753
|
+
ui.endedBy(ending, (await activeOrgName()) || "your workspace", endingRefusal);
|
|
693
754
|
await previewStop();
|
|
694
|
-
process.exitCode = ending
|
|
755
|
+
process.exitCode = previewExitCode(ending);
|
|
695
756
|
}
|
|
696
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
|
+
}
|
|
697
766
|
/** A `--port` or `--dev-command` value the schema refuses, before anything runs. */
|
|
698
767
|
function previewFlagRefusal(port, devCommand) {
|
|
699
768
|
const portMessage = port === undefined
|
|
@@ -1505,8 +1574,12 @@ function journal(exitCode, error) {
|
|
|
1505
1574
|
// a notice is worth nothing if it costs a second on every publish.
|
|
1506
1575
|
try {
|
|
1507
1576
|
const cached = loadCache();
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
+
}
|
|
1510
1583
|
if (!isFresh(cached, Date.now()))
|
|
1511
1584
|
refreshUpdateCache();
|
|
1512
1585
|
}
|
|
@@ -1598,6 +1671,15 @@ main()
|
|
|
1598
1671
|
journal(EXIT.NOT_AUTHENTICATED, err);
|
|
1599
1672
|
process.exit(EXIT.NOT_AUTHENTICATED);
|
|
1600
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
|
+
}
|
|
1601
1683
|
if (isAuthRefused(err)) {
|
|
1602
1684
|
for (const l of authExpiredLines())
|
|
1603
1685
|
console.error(l);
|
package/dist/cli/usage-text.js
CHANGED
|
@@ -11,9 +11,10 @@ export const USAGE_TEXT = `Vincentt CLI: the local-first AR dev loop\n\n` +
|
|
|
11
11
|
` Wait for the developer to describe what to build.\n` +
|
|
12
12
|
` Full guide: npm view @vincentt-xr/cli readme\n\n` +
|
|
13
13
|
`Auth\n` +
|
|
14
|
-
` login Sign in; save an access token to ~/.vincentt/config.json\n` +
|
|
14
|
+
` login Sign in this machine; save an access token to ~/.vincentt/config.json\n` +
|
|
15
|
+
` [--label <name>] names this machine (default: OS, arch, date)\n` +
|
|
15
16
|
` [--api <url>] [--console <url>] to target a non-prod env\n` +
|
|
16
|
-
` logout
|
|
17
|
+
` logout Sign this machine out and remove the saved access token\n` +
|
|
17
18
|
` whoami Show the signed-in account\n\n` +
|
|
18
19
|
`Project\n` +
|
|
19
20
|
` init [dir] Scaffold a Vincentt XR app; no account needed, ever\n` +
|
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);
|