@vincentt-xr/cli 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +30 -0
- package/dist/cli/index.js +86 -12
- package/dist/domain/api.js +16 -12
- package/dist/domain/command.js +9 -0
- package/dist/dx/command.d.ts +74 -18
- package/dist/dx/command.js +140 -86
- package/dist/dx/consent.d.ts +25 -6
- package/dist/dx/consent.js +64 -35
- package/dist/dx/enablement.d.ts +63 -11
- package/dist/dx/enablement.js +93 -21
- package/dist/dx/report.d.ts +4 -24
- package/dist/dx/report.js +32 -25
- package/dist/dx/reportIo.js +6 -4
- package/dist/dx/send.js +6 -4
- package/dist/lib/backend.js +5 -10
- package/dist/lib/exitCodes.d.ts +6 -0
- package/dist/lib/exitCodes.js +6 -0
- package/dist/lib/terminalText.d.ts +6 -0
- package/dist/lib/terminalText.js +10 -0
- package/dist/lib/version.d.ts +28 -2
- package/dist/lib/version.js +30 -3
- package/dist/lib/whoami.js +3 -0
- package/dist/library/assetsCommand.js +4 -0
- package/dist/library/client.js +6 -8
- package/dist/login/authBackend.js +18 -15
- package/dist/login/loginCommand.js +31 -4
- package/dist/login/pkceLogin.d.ts +10 -0
- package/dist/login/pkceLogin.js +24 -1
- package/dist/platform/releaseFetch.js +36 -69
- package/dist/preview/api.js +9 -12
- package/dist/preview/command.d.ts +3 -2
- package/dist/preview/command.js +10 -5
- package/dist/preview/copy.d.ts +13 -1
- package/dist/preview/copy.js +31 -3
- package/dist/preview/proxy.js +5 -39
- package/dist/preview/runner.d.ts +31 -9
- package/dist/preview/runner.js +43 -15
- package/dist/publish/api.js +14 -14
- package/dist/publish/command.d.ts +3 -0
- package/dist/publish/command.js +74 -7
- package/dist/publish/copy.d.ts +15 -0
- package/dist/publish/copy.js +109 -19
- package/dist/publish/keyScan.d.ts +25 -0
- package/dist/publish/keyScan.js +88 -0
- package/dist/publish/manifest.d.ts +60 -19
- package/dist/publish/manifest.js +290 -71
- package/dist/publish/upload.d.ts +18 -0
- package/dist/publish/upload.js +38 -0
- package/dist/publish/weight.d.ts +23 -0
- package/dist/publish/weight.js +113 -0
- package/dist/shared/config.d.ts +6 -6
- package/dist/shared/credentialNames.d.ts +17 -0
- package/dist/shared/credentialNames.js +72 -0
- package/dist/shared/http.d.ts +34 -0
- package/dist/shared/http.js +98 -2
- package/dist/shared/upgradeError.d.ts +62 -0
- package/dist/shared/upgradeError.js +215 -0
- package/dist/tunnel/caps.d.ts +19 -0
- package/dist/tunnel/caps.js +52 -0
- package/dist/tunnel/cbor.d.ts +39 -12
- package/dist/tunnel/cbor.js +84 -10
- package/dist/tunnel/client.d.ts +56 -2
- package/dist/tunnel/client.js +160 -44
- package/dist/tunnel/frame.d.ts +1 -1
- package/dist/tunnel/frame.js +8 -4
- package/dist/tunnel/index.d.ts +3 -2
- package/dist/tunnel/index.js +3 -2
- package/package.json +4 -3
package/dist/dx/enablement.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Durable authorization for unattended `vincentt dx --send`.
|
|
3
3
|
*
|
|
4
|
-
* `--send` transmits only when the session is interactive, OR
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `--send` transmits only when the session is interactive, OR the creator's ACCOUNT
|
|
5
|
+
* carries the grant, OR — only when the account cannot be read — this machine's cached
|
|
6
|
+
* copy of that grant is valid. `enable` records the account grant at the terminal or in
|
|
7
|
+
* the browser; either way a person answers, never the caller.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The account grant is read from `/me` and cleared by `disable` or in the console, NOT by
|
|
10
|
+
* `logout`, since signing out of one machine is not a decision about the account. The
|
|
11
|
+
* machine copy is two fields in ~/.vincentt/config.json, written by `enable` only after
|
|
12
|
+
* the account grant succeeded, and cleared by `disable`, by `logout`, and by any `/me`
|
|
13
|
+
* answer that is not a grant. Neither has an expiry.
|
|
14
14
|
*
|
|
15
15
|
* Because there is no expiry, VISIBILITY is the entire mitigation, and the AGE is
|
|
16
16
|
* the load-bearing half of it: `ENABLED` alone reads as configuration, while
|
|
@@ -55,9 +55,6 @@ export function readEnablement(cfg, now) {
|
|
|
55
55
|
return DISABLED;
|
|
56
56
|
if (now >= DX_UNATTENDED_SUNSET)
|
|
57
57
|
return DISABLED;
|
|
58
|
-
// The local flag is by definition a grant made on THIS machine — `dx enable` is the
|
|
59
|
-
// only writer, whether it took the browser path or the terminal one. An account grant
|
|
60
|
-
// never lands in this file; it is read from /me and reported separately.
|
|
61
58
|
return { enabled: true, at: dxSendEnabledAt, source: "machine" };
|
|
62
59
|
}
|
|
63
60
|
/**
|
|
@@ -71,10 +68,10 @@ export function readEnablement(cfg, now) {
|
|
|
71
68
|
* stops being honored too, or "forgetting the sunset turns the feature off" would only be
|
|
72
69
|
* true of the local half.
|
|
73
70
|
*/
|
|
74
|
-
export function
|
|
71
|
+
export function readAccountEnablement(raw, now) {
|
|
75
72
|
if (typeof raw !== "object" || raw === null)
|
|
76
73
|
return DISABLED;
|
|
77
|
-
const { dxReportsConsent, dxReportsConsentAt } = raw;
|
|
74
|
+
const { dxReportsConsent, dxReportsConsentAt, dxReportsConsentVia } = raw;
|
|
78
75
|
if (dxReportsConsent !== true)
|
|
79
76
|
return DISABLED;
|
|
80
77
|
if (typeof dxReportsConsentAt !== "string")
|
|
@@ -83,7 +80,73 @@ export function readAccountConsent(raw, now) {
|
|
|
83
80
|
return DISABLED;
|
|
84
81
|
if (now >= DX_UNATTENDED_SUNSET)
|
|
85
82
|
return DISABLED;
|
|
86
|
-
|
|
83
|
+
// An unknown via is dropped, not rejected: it only decides the suffix, never the grant.
|
|
84
|
+
return dxReportsConsentVia === "cli" || dxReportsConsentVia === "console"
|
|
85
|
+
? {
|
|
86
|
+
enabled: true,
|
|
87
|
+
at: dxReportsConsentAt,
|
|
88
|
+
source: "account",
|
|
89
|
+
via: dxReportsConsentVia,
|
|
90
|
+
}
|
|
91
|
+
: { enabled: true, at: dxReportsConsentAt, source: "account" };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The account-first read shared by the send gate, `vincentt dx` and `vincentt version`
|
|
95
|
+
* (f7 Scenario 3, 3c). The account decides when it answers; a "not granted" answer clears
|
|
96
|
+
* this machine's copy so a revoke made elsewhere reaches it. Only when the account cannot
|
|
97
|
+
* be read does the local flag stand on its own.
|
|
98
|
+
*/
|
|
99
|
+
export async function resolveConsentView(deps) {
|
|
100
|
+
const signedIn = deps.auth !== undefined;
|
|
101
|
+
if (deps.auth !== undefined && deps.readAccount !== undefined) {
|
|
102
|
+
const { auth, readAccount } = deps;
|
|
103
|
+
const read = await boundedRead(Promise.resolve().then(() => readAccount(auth)), deps.timeoutMs);
|
|
104
|
+
if (read.kind === "granted") {
|
|
105
|
+
return {
|
|
106
|
+
shown: read.enablement,
|
|
107
|
+
local: await deps.local(),
|
|
108
|
+
checked: "account",
|
|
109
|
+
signedIn,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (read.kind === "not_granted") {
|
|
113
|
+
// A failed clear still reports off: the account said no, so nothing may send, and
|
|
114
|
+
// the next account read clears again.
|
|
115
|
+
await deps.clearLocal().catch(() => undefined);
|
|
116
|
+
return { shown: DISABLED, local: DISABLED, checked: "account", signedIn };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const local = await deps.local();
|
|
120
|
+
return { shown: local, local, checked: "machine", signedIn };
|
|
121
|
+
}
|
|
122
|
+
async function boundedRead(read, timeoutMs) {
|
|
123
|
+
const unreadable = { kind: "unreadable" };
|
|
124
|
+
const guarded = read.catch(() => unreadable);
|
|
125
|
+
if (timeoutMs === undefined)
|
|
126
|
+
return guarded;
|
|
127
|
+
let timer;
|
|
128
|
+
const expired = new Promise((resolve) => {
|
|
129
|
+
timer = setTimeout(() => resolve(unreadable), timeoutMs);
|
|
130
|
+
timer.unref?.();
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
return await Promise.race([guarded, expired]);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function dxConsentJson(view) {
|
|
140
|
+
const { shown } = view;
|
|
141
|
+
if (shown.enabled && shown.at !== undefined) {
|
|
142
|
+
return {
|
|
143
|
+
state: "on",
|
|
144
|
+
checked: view.checked,
|
|
145
|
+
at: shown.at,
|
|
146
|
+
...(shown.via !== undefined ? { via: shown.via } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { state: view.signedIn ? "off" : "unknown", checked: view.checked };
|
|
87
150
|
}
|
|
88
151
|
/**
|
|
89
152
|
* Read enablement from the real machine config.
|
|
@@ -116,8 +179,10 @@ export function grantedOn(at) {
|
|
|
116
179
|
/**
|
|
117
180
|
* The one visibility line, in the single pinned shape every human surface uses:
|
|
118
181
|
*
|
|
182
|
+
* Unattended send: ENABLED since 2026-04-01 (145 days) · granted from the CLI
|
|
119
183
|
* Unattended send: ENABLED since 2026-04-01 (145 days) · granted in the console
|
|
120
|
-
* Unattended send: ENABLED since 2026-04-01 (145 days) ·
|
|
184
|
+
* Unattended send: ENABLED since 2026-04-01 (145 days) · on your account
|
|
185
|
+
* Unattended send: ENABLED since 2026-04-01 (145 days) · this machine (account not checked)
|
|
121
186
|
*
|
|
122
187
|
* Returns undefined when not enabled. Nothing is printed in that case, on
|
|
123
188
|
* purpose: a line that prints unconditionally is a line readers learn to skip,
|
|
@@ -136,10 +201,17 @@ export function enablementLine(e, now) {
|
|
|
136
201
|
if (!e.enabled || e.at === undefined)
|
|
137
202
|
return undefined;
|
|
138
203
|
const days = ageInDays(e.at, now);
|
|
139
|
-
const where = e
|
|
140
|
-
? " · granted in the console"
|
|
141
|
-
: e.source === "machine"
|
|
142
|
-
? " · granted on this machine"
|
|
143
|
-
: "";
|
|
204
|
+
const where = sourceSuffix(e);
|
|
144
205
|
return `Unattended send: ENABLED since ${grantedOn(e.at)} (${days} ${days === 1 ? "day" : "days"})${where}`;
|
|
145
206
|
}
|
|
207
|
+
function sourceSuffix(e) {
|
|
208
|
+
if (e.source === "machine")
|
|
209
|
+
return " · this machine (account not checked)";
|
|
210
|
+
if (e.source !== "account")
|
|
211
|
+
return "";
|
|
212
|
+
if (e.via === "cli")
|
|
213
|
+
return " · granted from the CLI";
|
|
214
|
+
if (e.via === "console")
|
|
215
|
+
return " · granted in the console";
|
|
216
|
+
return " · on your account";
|
|
217
|
+
}
|
package/dist/dx/report.d.ts
CHANGED
|
@@ -1,25 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* `vincentt report "<what went wrong>"` — the creator's own words, sent.
|
|
3
|
-
*
|
|
4
|
-
* This exists because of a gap `dx --send` could not close. That command reports
|
|
5
|
-
* DETECTED friction: it reads the journal, finds clusters of retries and errors,
|
|
6
|
-
* and refuses when it finds none. But the friction a creator most wants to report
|
|
7
|
-
* is routinely the kind the journal cannot see — a command that exits 0 and puts
|
|
8
|
-
* the wrong thing on screen produces no error, no retry and no cluster. Asked to
|
|
9
|
-
* carry a note about one, `--send` had nothing to attach it to and said "no
|
|
10
|
-
* friction detected — nothing to send", which is the tool contradicting the
|
|
11
|
-
* person using it.
|
|
12
|
-
*
|
|
13
|
-
* So the note stops being a rider on a finding and becomes the payload. An
|
|
14
|
-
* observation carries no counts because there is nothing to count.
|
|
15
|
-
*
|
|
16
|
-
* The consent model is INHERITED, not relaxed. Sending is still gated on the
|
|
17
|
-
* same three arms as `--send` (a terminal, a local `dx enable`, or an account
|
|
18
|
-
* grant), the payload is still shown in full before it goes, and a note is still
|
|
19
|
-
* shown under a warning that it is transmitted exactly as written. This verb
|
|
20
|
-
* removes a refusal; it does not open a new door.
|
|
21
|
-
*/
|
|
22
|
-
import type { Enablement } from "./enablement.js";
|
|
1
|
+
import type { AccountRead, Enablement } from "./enablement.js";
|
|
23
2
|
import type { JournalEntry } from "./journal.js";
|
|
24
3
|
import { type BuiltWith } from "../platform/resolve.js";
|
|
25
4
|
/** An observation as it goes on the wire. Mirrors the API's dxObservationRequest. */
|
|
@@ -114,7 +93,8 @@ export interface ReportDeps {
|
|
|
114
93
|
accountConsent?: (auth: {
|
|
115
94
|
apiUrl: string;
|
|
116
95
|
pat: string;
|
|
117
|
-
}) => Promise<
|
|
96
|
+
}) => Promise<AccountRead>;
|
|
97
|
+
clearEnablement?: () => Promise<unknown>;
|
|
118
98
|
/**
|
|
119
99
|
* The version set. Injectable so a fixed set — or a REJECTING promise, the arm
|
|
120
100
|
* proving a fault omits — can be driven with no filesystem. Defaults to the one
|
|
@@ -129,6 +109,6 @@ export interface ReportDeps {
|
|
|
129
109
|
* Same order as `dx --send`, for the same reason — a creator who cannot see what
|
|
130
110
|
* leaves their machine before it leaves has not agreed to anything:
|
|
131
111
|
*
|
|
132
|
-
* bound-check →
|
|
112
|
+
* bound-check → auth → GATE → RESOLVE → build → SHOW → ask → post
|
|
133
113
|
*/
|
|
134
114
|
export declare function reportCommand(deps: ReportDeps, options: ReportOptions): Promise<number>;
|
package/dist/dx/report.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vincentt report "<what went wrong>"` — the creator's own words, sent.
|
|
3
|
+
*
|
|
4
|
+
* This exists because of a gap `dx --send` could not close. That command reports
|
|
5
|
+
* DETECTED friction: it reads the journal, finds clusters of retries and errors,
|
|
6
|
+
* and refuses when it finds none. But the friction a creator most wants to report
|
|
7
|
+
* is routinely the kind the journal cannot see — a command that exits 0 and puts
|
|
8
|
+
* the wrong thing on screen produces no error, no retry and no cluster. Asked to
|
|
9
|
+
* carry a note about one, `--send` had nothing to attach it to and said "no
|
|
10
|
+
* friction detected — nothing to send", which is the tool contradicting the
|
|
11
|
+
* person using it.
|
|
12
|
+
*
|
|
13
|
+
* So the note stops being a rider on a finding and becomes the payload. An
|
|
14
|
+
* observation carries no counts because there is nothing to count.
|
|
15
|
+
*
|
|
16
|
+
* The consent model is INHERITED, not relaxed. Sending is gated by the same
|
|
17
|
+
* `gateSend` as `--send` (a terminal, the account grant, or, only when the
|
|
18
|
+
* account cannot be read, this machine's copy of it), the payload is still shown in full before it goes, and a note is still
|
|
19
|
+
* shown under a warning that it is transmitted exactly as written. This verb
|
|
20
|
+
* removes a refusal; it does not open a new door.
|
|
21
|
+
*/
|
|
22
|
+
import { gateSend, SEND_REFUSAL_LINES } from "./command.js";
|
|
1
23
|
import { enablementLine } from "./enablement.js";
|
|
2
24
|
import { DXSendError, platformTag } from "./send.js";
|
|
3
25
|
import { resolveBuiltWith } from "../platform/resolve.js";
|
|
@@ -110,7 +132,7 @@ export function renderObservation(report) {
|
|
|
110
132
|
* Same order as `dx --send`, for the same reason — a creator who cannot see what
|
|
111
133
|
* leaves their machine before it leaves has not agreed to anything:
|
|
112
134
|
*
|
|
113
|
-
* bound-check →
|
|
135
|
+
* bound-check → auth → GATE → RESOLVE → build → SHOW → ask → post
|
|
114
136
|
*/
|
|
115
137
|
export async function reportCommand(deps, options) {
|
|
116
138
|
const note = options.note.trim();
|
|
@@ -128,28 +150,13 @@ export async function reportCommand(deps, options) {
|
|
|
128
150
|
deps.print(`That note is longer than ${MAX_NOTE} characters. Please shorten it.`);
|
|
129
151
|
return 1;
|
|
130
152
|
}
|
|
131
|
-
// The gate,
|
|
132
|
-
//
|
|
133
|
-
const localEnablement = deps.enablement?.() ?? { enabled: false };
|
|
134
|
-
const interactive = deps.interactive();
|
|
153
|
+
// The gate, shared with `dx --send`. An agent composing a note and confirming it
|
|
154
|
+
// with --yes on an account nobody granted is the case this stops.
|
|
135
155
|
const auth = await deps.auth();
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// Fail closed. An unreachable API never authorizes a send.
|
|
143
|
-
accountEnablement = { enabled: false };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const enablement = localEnablement.enabled
|
|
147
|
-
? localEnablement
|
|
148
|
-
: accountEnablement;
|
|
149
|
-
if (!interactive && !enablement.enabled) {
|
|
150
|
-
deps.print("Sending a report needs consent that has not been given on this account.");
|
|
151
|
-
deps.print("`vincentt dx enable` opens the Vincentt console so you can allow it.");
|
|
152
|
-
deps.print("It takes one click, and it works from here.");
|
|
156
|
+
const gate = await gateSend(deps, auth);
|
|
157
|
+
if (gate.arm === "refused") {
|
|
158
|
+
for (const l of SEND_REFUSAL_LINES)
|
|
159
|
+
deps.print(l);
|
|
153
160
|
return 1;
|
|
154
161
|
}
|
|
155
162
|
if (auth === undefined) {
|
|
@@ -179,9 +186,9 @@ export async function reportCommand(deps, options) {
|
|
|
179
186
|
// omission an empty project yields, and neither can fail a report.
|
|
180
187
|
const builtWith = await (deps.builtWith ?? (() => resolveBuiltWith(deps.cwd ?? process.cwd())))().catch(() => undefined);
|
|
181
188
|
const report = buildObservationReport(note, deps.cliVersion(), context, builtWith);
|
|
182
|
-
const unattended =
|
|
183
|
-
if (unattended) {
|
|
184
|
-
const line = enablementLine(enablement, deps.now());
|
|
189
|
+
const unattended = gate.arm === "unattended";
|
|
190
|
+
if (gate.arm === "unattended") {
|
|
191
|
+
const line = enablementLine(gate.enablement, deps.now());
|
|
185
192
|
if (line !== undefined) {
|
|
186
193
|
deps.print("");
|
|
187
194
|
deps.print(`${line}. Not asking.`);
|
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/version.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Enablement } from "../dx/enablement.js";
|
|
1
|
+
import type { AccountRead, Enablement, EnablementSource, EnablementVia } from "../dx/enablement.js";
|
|
2
2
|
export interface VersionReport {
|
|
3
3
|
cliVersion: string;
|
|
4
4
|
nodeVersion: string;
|
|
@@ -9,10 +9,13 @@ export interface VersionReport {
|
|
|
9
9
|
ignoredBindingApiUrl?: string;
|
|
10
10
|
/** True when the resolved host is the built-in fallback (nothing configured). */
|
|
11
11
|
isDefault: boolean;
|
|
12
|
-
/** True when
|
|
12
|
+
/** True when DX reports may be sent unattended, from the account-first answer. */
|
|
13
13
|
dxSendEnabled: boolean;
|
|
14
14
|
/** ISO8601 UTC of the grant. Present only when enabled. */
|
|
15
15
|
dxSendEnabledAt?: string;
|
|
16
|
+
/** Where that answer came from, and where the account grant was made. */
|
|
17
|
+
dxSource?: EnablementSource;
|
|
18
|
+
dxVia?: EnablementVia;
|
|
16
19
|
/** The instant the age is computed against, so renderVersion stays pure. */
|
|
17
20
|
dxAgeAsOf: number;
|
|
18
21
|
}
|
|
@@ -50,4 +53,27 @@ export declare function renderVersion(r: VersionReport): string[];
|
|
|
50
53
|
export declare function versionReport(cwd: string, opts: {
|
|
51
54
|
cliVersion: string;
|
|
52
55
|
allowCustomApi?: boolean;
|
|
56
|
+
/** The account read; absent reports this machine's flag only. */
|
|
57
|
+
consent?: VersionConsentDeps;
|
|
53
58
|
}): Promise<VersionReport>;
|
|
59
|
+
/** The account bound on `version` (f7 Scenario 3c). */
|
|
60
|
+
export declare const VERSION_ACCOUNT_TIMEOUT_MS = 3000;
|
|
61
|
+
export interface VersionConsentDeps {
|
|
62
|
+
auth: () => Promise<{
|
|
63
|
+
apiUrl: string;
|
|
64
|
+
pat: string;
|
|
65
|
+
} | undefined>;
|
|
66
|
+
readAccount: (auth: {
|
|
67
|
+
apiUrl: string;
|
|
68
|
+
pat: string;
|
|
69
|
+
}) => Promise<AccountRead>;
|
|
70
|
+
clearLocal: () => Promise<unknown>;
|
|
71
|
+
local?: () => Promise<Enablement>;
|
|
72
|
+
timeoutMs?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The enablement `version` shows: the account's answer when it can be read within the
|
|
76
|
+
* bound, else this machine's flag. Never throws and never waits past the bound, because
|
|
77
|
+
* `version` is the command people run when something is already broken.
|
|
78
|
+
*/
|
|
79
|
+
export declare function versionEnablement(now: number, consent: VersionConsentDeps | undefined): Promise<Enablement>;
|
package/dist/lib/version.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// misconfiguration. So this reports the resolved host AND its provenance, using
|
|
8
8
|
// the same decideApiUrl the real verbs use; a version command that re-derived
|
|
9
9
|
// the URL could disagree with them.
|
|
10
|
-
import { enablementLine, loadEnablement } from "../dx/enablement.js";
|
|
10
|
+
import { enablementLine, loadEnablement, resolveConsentView } from "../dx/enablement.js";
|
|
11
11
|
import { decideApiUrl, loadMachineConfig, locateProjectBinding, } from "../shared/config.js";
|
|
12
12
|
/**
|
|
13
13
|
* Decide what to report. Pure, so the transcript is testable without a home
|
|
@@ -45,6 +45,8 @@ export function buildVersionReport(env) {
|
|
|
45
45
|
isDefault,
|
|
46
46
|
dxSendEnabled: enablement.enabled,
|
|
47
47
|
dxSendEnabledAt: enablement.enabled ? enablement.at : undefined,
|
|
48
|
+
dxSource: enablement.enabled ? enablement.source : undefined,
|
|
49
|
+
dxVia: enablement.enabled ? enablement.via : undefined,
|
|
48
50
|
dxAgeAsOf: env.now ?? Date.now(),
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -68,7 +70,7 @@ export function renderVersion(r) {
|
|
|
68
70
|
// exactly when a forgotten enablement is most likely to be discovered. Printed
|
|
69
71
|
// only when enabled — see the note in dx/enablement.ts on why there is no `off`
|
|
70
72
|
// counterpart.
|
|
71
|
-
const dx = enablementLine({ enabled: r.dxSendEnabled, at: r.dxSendEnabledAt }, r.dxAgeAsOf);
|
|
73
|
+
const dx = enablementLine({ enabled: r.dxSendEnabled, at: r.dxSendEnabledAt, source: r.dxSource, via: r.dxVia }, r.dxAgeAsOf);
|
|
72
74
|
if (dx !== undefined)
|
|
73
75
|
out.push("", ` ${dx}`);
|
|
74
76
|
out.push("");
|
|
@@ -106,7 +108,32 @@ export async function versionReport(cwd, opts) {
|
|
|
106
108
|
allowCustomApi: opts.allowCustomApi,
|
|
107
109
|
nodeVersion: process.version,
|
|
108
110
|
cliVersion: opts.cliVersion,
|
|
109
|
-
enablement: await
|
|
111
|
+
enablement: await versionEnablement(now, opts.consent),
|
|
110
112
|
now,
|
|
111
113
|
});
|
|
112
114
|
}
|
|
115
|
+
/** The account bound on `version` (f7 Scenario 3c). */
|
|
116
|
+
export const VERSION_ACCOUNT_TIMEOUT_MS = 3_000;
|
|
117
|
+
/**
|
|
118
|
+
* The enablement `version` shows: the account's answer when it can be read within the
|
|
119
|
+
* bound, else this machine's flag. Never throws and never waits past the bound, because
|
|
120
|
+
* `version` is the command people run when something is already broken.
|
|
121
|
+
*/
|
|
122
|
+
export async function versionEnablement(now, consent) {
|
|
123
|
+
const local = consent?.local ?? (() => loadEnablement(now));
|
|
124
|
+
if (consent === undefined)
|
|
125
|
+
return local();
|
|
126
|
+
try {
|
|
127
|
+
const view = await resolveConsentView({
|
|
128
|
+
auth: await consent.auth().catch(() => undefined),
|
|
129
|
+
readAccount: consent.readAccount,
|
|
130
|
+
local,
|
|
131
|
+
clearLocal: consent.clearLocal,
|
|
132
|
+
timeoutMs: consent.timeoutMs ?? VERSION_ACCOUNT_TIMEOUT_MS,
|
|
133
|
+
});
|
|
134
|
+
return view.shown;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return local().catch(() => ({ enabled: false }));
|
|
138
|
+
}
|
|
139
|
+
}
|
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.
|