@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/MIGRATION.md
CHANGED
|
@@ -3,6 +3,36 @@
|
|
|
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.22.0
|
|
7
|
+
|
|
8
|
+
required: false
|
|
9
|
+
No action required. Behaviour that changed:
|
|
10
|
+
- `vincentt dx enable` at a terminal now asks `Allow unattended reports on your account? [y/N]` and, on `y`, records the grant on your Vincentt account first, then sets this machine. Success reads `Enabled for your account. Reports may now be sent without a prompt from any machine signed in to it.`
|
|
11
|
+
- If the account does not record the grant, `vincentt dx enable` changes nothing and exits 1 with `✗ Vincentt did not record your consent.`, `✗ Too many consent changes. Wait a minute and try again.`, or `✗ Could not reach Vincentt to record your consent.` A refused sign-in exits 4 and a too-old CLI exits 5, as on other verbs.
|
|
12
|
+
- Unattended `vincentt dx --send` and `vincentt report` now check the account first. An account grant sends; an account that is not granted refuses (exit 1) and clears this machine's setting, so turning reports off in the console or on another machine reaches this one. This machine's setting is used on its own only when the account cannot be reached. An interactive session still asks and makes no account check.
|
|
13
|
+
- The `Unattended send: ENABLED since <day> (<n> days)` line now ends with where the answer came from: `· granted from the CLI`, `· granted in the console`, `· on your account`, or `· this machine (account not checked)`. `vincentt dx` and `vincentt version` check the account for up to 3 seconds first; `vincentt version` never fails because of that check.
|
|
14
|
+
- `vincentt dx --json` adds `dxConsent`: `{"state": "on" | "off" | "unknown", "checked": "account" | "machine", "at"?, "via"?}`. `dxSendEnabled` and `dxSendEnabledAt` still describe this machine's setting.
|
|
15
|
+
- `vincentt preview` has a new ending: when the preview server answers the tunnel with a reply this CLI cannot accept, the preview ends once with `✗ Preview stopped: the preview server sent an invalid reply. Update the CLI or try again later.` (exit 1) instead of reconnecting every few seconds indefinitely. The screen names the upgrade command and `vincentt preview`.
|
|
16
|
+
- `vincentt publish` refuses the whole publish, listing the files, when a folder in the build output turns into a link to somewhere outside it while publish reads it. Let the build finish, then publish again.
|
|
17
|
+
- `vincentt publish` refuses to upload when the platform's upload link does not carry the checksum publish computed for that file; nothing is sent, and the screen names the file.
|
|
18
|
+
- A folder or file in the build output that publish cannot read now ends the publish with the "changed while publish was reading it" screen naming the path inside the build output, instead of a raw error message holding the full path.
|
|
19
|
+
- File names printed on publish refusal screens show bidirectional and direction-mark characters as visible `\uXXXX` escapes.
|
|
20
|
+
|
|
21
|
+
## 0.21.0
|
|
22
|
+
|
|
23
|
+
required: false
|
|
24
|
+
No action required unless a build output holds a credential file. Behaviour that changed:
|
|
25
|
+
- `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).
|
|
26
|
+
- `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.
|
|
27
|
+
- A file that changes or becomes a link while publish reads it is also refused.
|
|
28
|
+
- 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.
|
|
29
|
+
- `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.
|
|
30
|
+
- 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>`).
|
|
31
|
+
- 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.
|
|
32
|
+
- 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.
|
|
33
|
+
- `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.
|
|
34
|
+
- 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.
|
|
35
|
+
|
|
6
36
|
## 0.20.0
|
|
7
37
|
|
|
8
38
|
required: false
|
package/dist/cli/index.js
CHANGED
|
@@ -24,11 +24,12 @@ 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";
|
|
30
31
|
import { whoamiCommand } from "../lib/whoami.js";
|
|
31
|
-
import { renderVersion, versionReport } from "../lib/version.js";
|
|
32
|
+
import { renderVersion, VERSION_ACCOUNT_TIMEOUT_MS, versionReport, } from "../lib/version.js";
|
|
32
33
|
import { diagQuery, diagRead, previewStart, previewStop, previewStopSignal, projectCreate, } from "../lib/lifecycle.js";
|
|
33
34
|
import { publishCommand } from "../publish/command.js";
|
|
34
35
|
import { stdinAsk, stdinConfirm } from "../publish/prompt.js";
|
|
@@ -52,7 +53,7 @@ import * as feedbackCopy from "../lib/feedbackCopy.js";
|
|
|
52
53
|
import { record as recordInvocation } from "../dx/journal.js";
|
|
53
54
|
import { dxDisableCommand, dxEnableCommand, dxSendCommand, parseDxOptions, runDx, sinceUsageLines, } from "../dx/command.js";
|
|
54
55
|
import { httpConsentBackend, runConsentInBrowser } from "../dx/consent.js";
|
|
55
|
-
import { DX_UNATTENDED_SUNSET, loadEnablement } from "../dx/enablement.js";
|
|
56
|
+
import { DX_UNATTENDED_SUNSET, loadEnablement, resolveConsentView, } from "../dx/enablement.js";
|
|
56
57
|
import { journalEnabled, journalPath, readJournal } from "../dx/journal.js";
|
|
57
58
|
import { postReport } from "../dx/send.js";
|
|
58
59
|
import { nudgeFor } from "../dx/nudge.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
|
|
@@ -1095,14 +1139,22 @@ function cliVersion() {
|
|
|
1095
1139
|
}
|
|
1096
1140
|
}
|
|
1097
1141
|
async function version() {
|
|
1142
|
+
const now = Date.now();
|
|
1098
1143
|
const report = await versionReport(projectDir(), {
|
|
1099
1144
|
cliVersion: cliVersion(),
|
|
1100
1145
|
allowCustomApi: has("--allow-custom-api"),
|
|
1146
|
+
consent: {
|
|
1147
|
+
auth: dxAuth,
|
|
1148
|
+
readAccount: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now, VERSION_ACCOUNT_TIMEOUT_MS),
|
|
1149
|
+
clearLocal: () => writeDxEnablement(undefined),
|
|
1150
|
+
},
|
|
1101
1151
|
});
|
|
1102
1152
|
for (const line of renderVersion(report))
|
|
1103
1153
|
console.log(line);
|
|
1104
1154
|
}
|
|
1105
1155
|
// ---- dx ---------------------------------------------------------------------
|
|
1156
|
+
/** The account bound on `vincentt dx` (f7 Scenario 3c); the send gate uses the default. */
|
|
1157
|
+
const DX_ACCOUNT_TIMEOUT_MS = 3_000;
|
|
1106
1158
|
/**
|
|
1107
1159
|
* The API host + PAT for a dx call, or undefined when signed out.
|
|
1108
1160
|
*
|
|
@@ -1153,6 +1205,7 @@ async function dx() {
|
|
|
1153
1205
|
onProgress: (l) => console.log(l),
|
|
1154
1206
|
});
|
|
1155
1207
|
},
|
|
1208
|
+
grantAccountConsent: (auth) => consent.grant(auth.apiUrl, auth.pat, now),
|
|
1156
1209
|
revokeAccountConsent: (auth) => consent.revoke(auth.apiUrl, auth.pat),
|
|
1157
1210
|
};
|
|
1158
1211
|
process.exitCode = options.enable
|
|
@@ -1170,7 +1223,15 @@ async function dx() {
|
|
|
1170
1223
|
// relaxation, not throw through it.
|
|
1171
1224
|
const enablement = await loadEnablement(now);
|
|
1172
1225
|
if (!options.send) {
|
|
1173
|
-
|
|
1226
|
+
// The report tail and `dxConsent` read the account first, bounded at 3 s.
|
|
1227
|
+
const view = await resolveConsentView({
|
|
1228
|
+
auth: await dxAuth(),
|
|
1229
|
+
readAccount: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now, DX_ACCOUNT_TIMEOUT_MS),
|
|
1230
|
+
local: () => enablement,
|
|
1231
|
+
clearLocal: () => writeDxEnablement(undefined),
|
|
1232
|
+
timeoutMs: DX_ACCOUNT_TIMEOUT_MS,
|
|
1233
|
+
});
|
|
1234
|
+
process.exitCode = runDx(argv, (l) => console.log(l), () => view.local, () => view);
|
|
1174
1235
|
return;
|
|
1175
1236
|
}
|
|
1176
1237
|
// `--send` is the one path out of this machine, so it resolves auth the way
|
|
@@ -1187,10 +1248,9 @@ async function dx() {
|
|
|
1187
1248
|
confirm: (q) => stdinConfirm()(`${q} [y/N] `, false),
|
|
1188
1249
|
interactive,
|
|
1189
1250
|
enablement: () => enablement,
|
|
1190
|
-
// The
|
|
1191
|
-
// first two have not already allowed the send, and allowed to throw — a /me that
|
|
1192
|
-
// is unreachable falls back to the other two arms rather than blocking.
|
|
1251
|
+
// The account decides when it answers; never read on an interactive session.
|
|
1193
1252
|
accountConsent: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now),
|
|
1253
|
+
clearEnablement: () => writeDxEnablement(undefined),
|
|
1194
1254
|
// `--dir`-or-cwd, the same resolution every other verb uses, so `--dir`
|
|
1195
1255
|
// works on a send as it does everywhere else.
|
|
1196
1256
|
cwd: projectDir(),
|
|
@@ -1232,6 +1292,7 @@ async function report() {
|
|
|
1232
1292
|
interactive,
|
|
1233
1293
|
enablement: () => enablement,
|
|
1234
1294
|
accountConsent: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now),
|
|
1295
|
+
clearEnablement: () => writeDxEnablement(undefined),
|
|
1235
1296
|
// `--dir`-or-cwd, the same resolution every other verb uses, so `--dir`
|
|
1236
1297
|
// works on a report as it does everywhere else.
|
|
1237
1298
|
cwd: projectDir(),
|
|
@@ -1530,8 +1591,12 @@ function journal(exitCode, error) {
|
|
|
1530
1591
|
// a notice is worth nothing if it costs a second on every publish.
|
|
1531
1592
|
try {
|
|
1532
1593
|
const cached = loadCache();
|
|
1533
|
-
|
|
1534
|
-
|
|
1594
|
+
// Suppressed on a version refusal: the ending already names the number that
|
|
1595
|
+
// matters, and a stale cache could name a `latest` below the floor.
|
|
1596
|
+
if (exitCode !== EXIT.UPGRADE_REQUIRED) {
|
|
1597
|
+
for (const l of updateNotice(cliVersion(), cached?.latest))
|
|
1598
|
+
console.error(l);
|
|
1599
|
+
}
|
|
1535
1600
|
if (!isFresh(cached, Date.now()))
|
|
1536
1601
|
refreshUpdateCache();
|
|
1537
1602
|
}
|
|
@@ -1623,6 +1688,15 @@ main()
|
|
|
1623
1688
|
journal(EXIT.NOT_AUTHENTICATED, err);
|
|
1624
1689
|
process.exit(EXIT.NOT_AUTHENTICATED);
|
|
1625
1690
|
}
|
|
1691
|
+
// L22's generic screen, for every verb without its own arm. Prose on the
|
|
1692
|
+
// verb's usual channel, or stderr under --json so stdout stays a document.
|
|
1693
|
+
if (err instanceof UpgradeRequiredError) {
|
|
1694
|
+
const say = has("--json") ? console.error : console.log;
|
|
1695
|
+
for (const l of upgradeRefusalLines(err))
|
|
1696
|
+
say(l);
|
|
1697
|
+
journal(EXIT.UPGRADE_REQUIRED, err);
|
|
1698
|
+
process.exit(EXIT.UPGRADE_REQUIRED);
|
|
1699
|
+
}
|
|
1626
1700
|
if (isAuthRefused(err)) {
|
|
1627
1701
|
for (const l of authExpiredLines())
|
|
1628
1702
|
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.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ConsentDecision } from "./consent.js";
|
|
2
|
-
import type { Enablement } from "./enablement.js";
|
|
1
|
+
import type { ConsentDecision, GrantResult } from "./consent.js";
|
|
2
|
+
import type { AccountRead, ConsentView, Enablement } from "./enablement.js";
|
|
3
3
|
import type { JournalEntry, JournalWindow } from "./journal.js";
|
|
4
4
|
import type { WireReport } from "./send.js";
|
|
5
5
|
import { type BuiltWith } from "../platform/resolve.js";
|
|
@@ -11,10 +11,15 @@ export interface DxDeps {
|
|
|
11
11
|
enabled: () => boolean;
|
|
12
12
|
path: () => string;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* the journal need not resolve it; absent reads as not enabled.
|
|
14
|
+
* This machine's copy of the grant (the local flag). Optional so the many callers that
|
|
15
|
+
* only read the journal need not resolve it; absent reads as not enabled.
|
|
16
16
|
*/
|
|
17
17
|
enablement?: () => Enablement;
|
|
18
|
+
/**
|
|
19
|
+
* The account-first answer the report tail and `dxConsent` show. Absent falls back to
|
|
20
|
+
* the local flag, reported as checked on this machine only.
|
|
21
|
+
*/
|
|
22
|
+
consentView?: () => ConsentView;
|
|
18
23
|
}
|
|
19
24
|
/** How far back a report looks. */
|
|
20
25
|
export interface DxWindow {
|
|
@@ -53,7 +58,7 @@ export declare function sinceUsageLines(raw: string): string[];
|
|
|
53
58
|
export declare function parseDxOptions(args: string[]): DxOptions;
|
|
54
59
|
export declare function dxCommand(deps: DxDeps, options: DxOptions): number;
|
|
55
60
|
/** Wire the command to the real journal. */
|
|
56
|
-
export declare function runDx(args: string[], print: (line: string) => void, enablement?: () => Enablement): number;
|
|
61
|
+
export declare function runDx(args: string[], print: (line: string) => void, enablement?: () => Enablement, consentView?: () => ConsentView): number;
|
|
57
62
|
export interface DxEnableDeps {
|
|
58
63
|
print: (line: string) => void;
|
|
59
64
|
now: () => number;
|
|
@@ -89,6 +94,14 @@ export interface DxEnableDeps {
|
|
|
89
94
|
* no socket, no browser, and no clock to test.
|
|
90
95
|
*/
|
|
91
96
|
consentInBrowser?: () => Promise<ConsentDecision>;
|
|
97
|
+
/**
|
|
98
|
+
* POST /me/dx-consent with the PAT, on the terminal path. Rejects only with the version
|
|
99
|
+
* refusal or the sign-in error; every other outcome is a GrantResult.
|
|
100
|
+
*/
|
|
101
|
+
grantAccountConsent?: (auth: {
|
|
102
|
+
apiUrl: string;
|
|
103
|
+
pat: string;
|
|
104
|
+
}) => Promise<GrantResult>;
|
|
92
105
|
/**
|
|
93
106
|
* DELETE /me/dx-consent. Used only by `disable`, and allowed to throw — the caller
|
|
94
107
|
* treats a network failure as "local revoke still stands", never as a reason to leave
|
|
@@ -103,11 +116,13 @@ export interface DxEnableDeps {
|
|
|
103
116
|
* `vincentt dx enable` — grant durable permission to send DX reports without asking
|
|
104
117
|
* first.
|
|
105
118
|
*
|
|
106
|
-
* TWO PATHS, and which one runs is decided by whether there is a
|
|
119
|
+
* TWO PATHS to ONE account grant, and which one runs is decided by whether there is a
|
|
120
|
+
* terminal to ask in:
|
|
107
121
|
*
|
|
108
|
-
* - At a TTY: the
|
|
109
|
-
*
|
|
110
|
-
*
|
|
122
|
+
* - At a TTY: the terminal prompt. A person is already here and can read the
|
|
123
|
+
* disclosure where they stand; sending them to a browser would be ceremony. On `y`
|
|
124
|
+
* the CLI POSTs /me/dx-consent with the PAT, and writes the local flag only after
|
|
125
|
+
* the account recorded the grant.
|
|
111
126
|
* - With no TTY: the BROWSER path. This is the case the feature exists for — a
|
|
112
127
|
* creator driving the CLI through a coding agent has no terminal, so the old code
|
|
113
128
|
* refused and the flow dead-ended. It now opens the console, where a human can
|
|
@@ -130,8 +145,8 @@ export declare function dxEnableCommand(deps: DxEnableDeps, sunset: number): Pro
|
|
|
130
145
|
* Clears BOTH local keys AND revokes the account grant when there is a PAT to do it
|
|
131
146
|
* with. Both halves are needed: a creator who runs `disable` means "stop sending", and
|
|
132
147
|
* clearing only the local flag would leave the account grant standing, so the very next
|
|
133
|
-
* `--send` would be authorized by
|
|
134
|
-
* worked.
|
|
148
|
+
* `--send` would be authorized by the account and the command would appear not to
|
|
149
|
+
* have worked.
|
|
135
150
|
*
|
|
136
151
|
* ORDER IS DELIBERATE: local first, network second. The local clear is the one that
|
|
137
152
|
* cannot fail and the one that stops THIS machine, which is the machine the creator is
|
|
@@ -168,18 +183,19 @@ export interface DxSendDeps extends DxDeps {
|
|
|
168
183
|
* convenience, not authorization.
|
|
169
184
|
*/
|
|
170
185
|
interactive: () => boolean;
|
|
171
|
-
/**
|
|
186
|
+
/** This machine's flag, read strictly. Absent reads as not enabled. */
|
|
172
187
|
enablement?: () => Enablement;
|
|
173
188
|
/**
|
|
174
|
-
* The ACCOUNT's consent, read from GET /me.
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* not wired at all) reads as not consented, never as "unknown, so allow".
|
|
189
|
+
* The ACCOUNT's consent, read from GET /me. Allowed to throw; a throw reads as
|
|
190
|
+
* unreadable. Absent reads as unreadable too: it can only let a valid local flag stand,
|
|
191
|
+
* never authorize on its own.
|
|
178
192
|
*/
|
|
179
193
|
accountConsent?: (auth: {
|
|
180
194
|
apiUrl: string;
|
|
181
195
|
pat: string;
|
|
182
|
-
}) => Promise<
|
|
196
|
+
}) => Promise<AccountRead>;
|
|
197
|
+
/** Clears this machine's flag when the account answers "not granted". */
|
|
198
|
+
clearEnablement?: () => Promise<unknown>;
|
|
183
199
|
/**
|
|
184
200
|
* The version set. Injectable so a fixed set — or a REJECTING promise, the arm
|
|
185
201
|
* proving a fault omits — can be driven with no filesystem. Defaults to the one
|
|
@@ -188,6 +204,46 @@ export interface DxSendDeps extends DxDeps {
|
|
|
188
204
|
builtWith?: () => Promise<BuiltWith | undefined>;
|
|
189
205
|
cwd?: string;
|
|
190
206
|
}
|
|
207
|
+
/** What the send gate needs. `dx --send` and `report` both satisfy it. */
|
|
208
|
+
export interface SendGateDeps {
|
|
209
|
+
interactive: () => boolean;
|
|
210
|
+
enablement?: () => Enablement;
|
|
211
|
+
accountConsent?: (auth: {
|
|
212
|
+
apiUrl: string;
|
|
213
|
+
pat: string;
|
|
214
|
+
}) => Promise<AccountRead>;
|
|
215
|
+
clearEnablement?: () => Promise<unknown>;
|
|
216
|
+
}
|
|
217
|
+
export type SendGate = {
|
|
218
|
+
arm: "interactive";
|
|
219
|
+
} | {
|
|
220
|
+
arm: "unattended";
|
|
221
|
+
enablement: Enablement;
|
|
222
|
+
} | {
|
|
223
|
+
arm: "refused";
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* The one consent gate both send paths call (f7 Scenario 3), checked in order:
|
|
227
|
+
*
|
|
228
|
+
* 1. an interactive session → ask, then send. No /me read at all, so a creator at their
|
|
229
|
+
* own terminal is never blocked by an API being slow or down.
|
|
230
|
+
* 2. signed in and GET /me answers: a strict grant sends (source: account); any other
|
|
231
|
+
* 200 clears this machine's flag and refuses.
|
|
232
|
+
* 3. /me cannot be read and this machine's flag is valid → send (source: machine).
|
|
233
|
+
*
|
|
234
|
+
* An unreadable /me never authorizes by itself; it only lets an existing local flag stand.
|
|
235
|
+
*/
|
|
236
|
+
export declare function gateSend(deps: SendGateDeps, auth: {
|
|
237
|
+
apiUrl: string;
|
|
238
|
+
pat: string;
|
|
239
|
+
} | undefined): Promise<SendGate>;
|
|
240
|
+
/**
|
|
241
|
+
* The gate's refusal. It NAMES the enabling command: a refusal is an answer to a
|
|
242
|
+
* command the caller typed, which makes it SOLICITED output — the prohibition is on
|
|
243
|
+
* UNBIDDEN output instructing a caller. A refusal naming nothing leaves a human at
|
|
244
|
+
* exit 1 with no path forward, for no security gain.
|
|
245
|
+
*/
|
|
246
|
+
export declare const SEND_REFUSAL_LINES: readonly string[];
|
|
191
247
|
/**
|
|
192
248
|
* `vincentt dx --send`.
|
|
193
249
|
*
|
|
@@ -195,6 +251,6 @@ export interface DxSendDeps extends DxDeps {
|
|
|
195
251
|
* who cannot see what leaves their machine before it leaves has not agreed to
|
|
196
252
|
* anything:
|
|
197
253
|
*
|
|
198
|
-
* friction →
|
|
254
|
+
* friction → auth → GATE → RESOLVE → build → SHOW → ask → post
|
|
199
255
|
*/
|
|
200
256
|
export declare function dxSendCommand(deps: DxSendDeps, options: DxOptions): Promise<number>;
|