@vincentt-xr/cli 0.21.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 +15 -0
- package/dist/cli/index.js +23 -6
- package/dist/dx/command.d.ts +74 -18
- package/dist/dx/command.js +137 -86
- package/dist/dx/consent.d.ts +25 -6
- package/dist/dx/consent.js +58 -25
- 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/lib/version.d.ts +28 -2
- package/dist/lib/version.js +30 -3
- package/dist/preview/command.js +2 -0
- package/dist/preview/copy.d.ts +5 -0
- package/dist/preview/copy.js +15 -0
- package/dist/preview/runner.d.ts +6 -0
- package/dist/preview/runner.js +1 -0
- package/dist/publish/command.js +5 -2
- package/dist/publish/copy.d.ts +6 -0
- package/dist/publish/copy.js +27 -6
- package/dist/publish/manifest.d.ts +11 -2
- package/dist/publish/manifest.js +45 -9
- package/dist/publish/upload.d.ts +10 -0
- package/dist/publish/upload.js +23 -0
- package/dist/shared/config.d.ts +6 -6
- package/dist/tunnel/client.d.ts +11 -3
- package/dist/tunnel/client.js +11 -8
- package/package.json +1 -1
package/dist/dx/consent.d.ts
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
|
-
import { type Enablement } from "./enablement.js";
|
|
1
|
+
import { type AccountRead, type Enablement } from "./enablement.js";
|
|
2
|
+
/** POST /me/dx-consent, sorted into the outcomes `dx enable` answers differently. */
|
|
3
|
+
export type GrantResult = {
|
|
4
|
+
kind: "granted";
|
|
5
|
+
enablement: Enablement;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "not_granted";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "rate_limited";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "unreachable";
|
|
12
|
+
};
|
|
2
13
|
/**
|
|
3
14
|
* The account-consent seam.
|
|
4
15
|
*
|
|
5
|
-
*
|
|
16
|
+
* Both reads return parsed results rather than the raw envelope so the strict
|
|
6
17
|
* `=== true` interpretation happens in exactly one place and cannot be re-derived
|
|
7
18
|
* loosely by a second caller.
|
|
8
19
|
*/
|
|
9
20
|
export interface ConsentBackend {
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
/**
|
|
22
|
+
* GET /me → the account's consent, read strictly. A 426, a 5xx, a network error or a
|
|
23
|
+
* timeout is `unreadable`: the CLI floor refuses /me while reports stay floor-exempt,
|
|
24
|
+
* so a below-floor CLI must fall back to its local flag rather than stop. A 401 still
|
|
25
|
+
* rejects with the shared sign-in error.
|
|
26
|
+
*/
|
|
27
|
+
read(apiUrl: string, pat: string, now: number, timeoutMs?: number): Promise<AccountRead>;
|
|
28
|
+
/**
|
|
29
|
+
* POST /me/dx-consent — grant. A re-grant refreshes the timestamp. Rejects only with
|
|
30
|
+
* the version refusal (426) or the sign-in error (401); every other outcome is a result.
|
|
31
|
+
*/
|
|
32
|
+
grant(apiUrl: string, pat: string, now: number): Promise<GrantResult>;
|
|
14
33
|
/** DELETE /me/dx-consent — revoke. Never fails on already-revoked. */
|
|
15
34
|
revoke(apiUrl: string, pat: string): Promise<void>;
|
|
16
35
|
}
|
package/dist/dx/consent.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The ACCOUNT
|
|
2
|
+
* The ACCOUNT grant for unattended sends: the authority, which the local flag caches.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* The grant is PER-ACCOUNT, not per-machine: one decision covers every machine the
|
|
10
|
-
* creator signs in on. That is a wider blast radius than the local flag, which is why
|
|
11
|
-
* the console page carries the full disclosure (including that an agent may compose a
|
|
12
|
-
* note the user has not read) and why the visibility line names the console as the
|
|
13
|
-
* source — a creator who wants it off needs to know that clearing a local flag is not
|
|
14
|
-
* enough.
|
|
4
|
+
* A person grants it at a terminal (`dx enable` POSTs /me/dx-consent with the PAT) or,
|
|
5
|
+
* with no terminal, on the console page the CLI opens. Either way the grant is
|
|
6
|
+
* PER-ACCOUNT: one decision covers every machine the creator signs in on, which is why
|
|
7
|
+
* both surfaces carry the full disclosure (including that an agent may compose a note
|
|
8
|
+
* the user has not read) and why the visibility line names where it was granted.
|
|
15
9
|
*
|
|
16
10
|
* Every call is isolated behind ConsentBackend so the command layer is unit-testable
|
|
17
11
|
* with no network.
|
|
@@ -21,10 +15,10 @@ import { waitForLoopbackCallback } from "../login/loopback.js";
|
|
|
21
15
|
import { browserPage } from "../login/pkceLogin.js";
|
|
22
16
|
import { stateMatches } from "../login/pkce.js";
|
|
23
17
|
import { getFreePort } from "../preview/net.js";
|
|
24
|
-
import { authFailureMessage } from "../shared/authError.js";
|
|
25
|
-
import { apiRequest } from "../shared/http.js";
|
|
26
|
-
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
27
|
-
import {
|
|
18
|
+
import { AuthExpiredError, authFailureMessage, rejectAuthExpired, } from "../shared/authError.js";
|
|
19
|
+
import { apiFetch, apiRequest, withDeadline } from "../shared/http.js";
|
|
20
|
+
import { rejectUpgradeRequired, UpgradeRequiredError } from "../shared/upgradeError.js";
|
|
21
|
+
import { readAccountEnablement, } from "./enablement.js";
|
|
28
22
|
async function safeText(res) {
|
|
29
23
|
try {
|
|
30
24
|
return (await res.text()).slice(0, 300);
|
|
@@ -54,19 +48,58 @@ export function httpConsentBackend() {
|
|
|
54
48
|
return res;
|
|
55
49
|
};
|
|
56
50
|
return {
|
|
57
|
-
async read(apiUrl, pat, now) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
async read(apiUrl, pat, now, timeoutMs) {
|
|
52
|
+
const fetchImpl = timeoutMs === undefined
|
|
53
|
+
? apiFetch
|
|
54
|
+
: withDeadline((input, init) => globalThis.fetch(input, init), timeoutMs);
|
|
55
|
+
let res;
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
res = await apiRequest({ apiUrl, pat }, "/me", {}, { authenticated: true }, fetchImpl);
|
|
59
|
+
if (res.status === 401)
|
|
60
|
+
rejectAuthExpired(res);
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
return { kind: "unreadable" };
|
|
63
|
+
body = await res.json();
|
|
61
64
|
}
|
|
62
|
-
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err instanceof AuthExpiredError)
|
|
67
|
+
throw err;
|
|
68
|
+
// A 200 whose body does not parse (a captive portal, a proxy page) is not an
|
|
69
|
+
// account answer, so it must not clear this machine's flag.
|
|
70
|
+
return { kind: "unreadable" };
|
|
71
|
+
}
|
|
72
|
+
const enablement = readAccountEnablement(unwrapMe(body), now);
|
|
73
|
+
return enablement.enabled
|
|
74
|
+
? { kind: "granted", enablement }
|
|
75
|
+
: { kind: "not_granted" };
|
|
63
76
|
},
|
|
64
77
|
async grant(apiUrl, pat, now) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
let res;
|
|
79
|
+
try {
|
|
80
|
+
res = await call(apiUrl, pat, "/me/dx-consent", { method: "POST" });
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (err instanceof UpgradeRequiredError)
|
|
84
|
+
throw err;
|
|
85
|
+
return { kind: "unreachable" };
|
|
86
|
+
}
|
|
87
|
+
rejectAuthExpired(res);
|
|
88
|
+
if (res.status === 429)
|
|
89
|
+
return { kind: "rate_limited" };
|
|
90
|
+
if (!res.ok)
|
|
91
|
+
return { kind: "unreachable" };
|
|
92
|
+
let body;
|
|
93
|
+
try {
|
|
94
|
+
body = await res.json();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return { kind: "not_granted" };
|
|
68
98
|
}
|
|
69
|
-
|
|
99
|
+
const enablement = readAccountEnablement(unwrapMe(body), now);
|
|
100
|
+
return enablement.enabled
|
|
101
|
+
? { kind: "granted", enablement }
|
|
102
|
+
: { kind: "not_granted" };
|
|
70
103
|
},
|
|
71
104
|
async revoke(apiUrl, pat) {
|
|
72
105
|
const res = await call(apiUrl, pat, "/me/dx-consent", { method: "DELETE" });
|
package/dist/dx/enablement.d.ts
CHANGED
|
@@ -11,21 +11,21 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const DX_UNATTENDED_SUNSET: number;
|
|
13
13
|
/**
|
|
14
|
-
* WHERE the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* The source is carried rather than inferred because the two have different blast radii
|
|
18
|
-
* and different places to revoke. A creator reading `ENABLED` on a machine they never
|
|
19
|
-
* ran `dx enable` on has no way to act on it without being told the grant lives in their
|
|
20
|
-
* account — they would go looking for a local flag that is not there.
|
|
14
|
+
* WHERE the answer came from. The account is the authority whenever GET /me answers;
|
|
15
|
+
* `machine` means this machine's cached copy was used because the account could not be
|
|
16
|
+
* read. The two are reported differently because only one of them was checked just now.
|
|
21
17
|
*/
|
|
22
|
-
export type EnablementSource = "
|
|
18
|
+
export type EnablementSource = "account" | "machine";
|
|
19
|
+
/** Where the person answered the account grant, as the api recorded it. */
|
|
20
|
+
export type EnablementVia = "cli" | "console";
|
|
23
21
|
export interface Enablement {
|
|
24
22
|
enabled: boolean;
|
|
25
23
|
/** The raw ISO8601 grant time. Present only when `enabled` is true. */
|
|
26
24
|
at?: string;
|
|
27
|
-
/** Where the
|
|
25
|
+
/** Where the answer came from. Present only when `enabled` is true. */
|
|
28
26
|
source?: EnablementSource;
|
|
27
|
+
/** Account grants only, and only when the api recorded it. */
|
|
28
|
+
via?: EnablementVia;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Decide enablement from an already-parsed config object. Pure, and STRICT:
|
|
@@ -49,7 +49,57 @@ export declare function readEnablement(cfg: unknown, now: number): Enablement;
|
|
|
49
49
|
* stops being honored too, or "forgetting the sunset turns the feature off" would only be
|
|
50
50
|
* true of the local half.
|
|
51
51
|
*/
|
|
52
|
-
export declare function
|
|
52
|
+
export declare function readAccountEnablement(raw: unknown, now: number): Enablement;
|
|
53
|
+
/** GET /me, as the consent gate and the visibility surfaces need it. */
|
|
54
|
+
export type AccountRead = {
|
|
55
|
+
kind: "granted";
|
|
56
|
+
enablement: Enablement;
|
|
57
|
+
} | {
|
|
58
|
+
kind: "not_granted";
|
|
59
|
+
} | {
|
|
60
|
+
kind: "unreadable";
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* What the consent surfaces show: the answer that decides (`shown`), this machine's copy
|
|
64
|
+
* (`local`, after any clear), and where the answer was checked.
|
|
65
|
+
*/
|
|
66
|
+
export interface ConsentView {
|
|
67
|
+
shown: Enablement;
|
|
68
|
+
local: Enablement;
|
|
69
|
+
checked: "account" | "machine";
|
|
70
|
+
signedIn: boolean;
|
|
71
|
+
}
|
|
72
|
+
export interface ConsentViewDeps {
|
|
73
|
+
auth: {
|
|
74
|
+
apiUrl: string;
|
|
75
|
+
pat: string;
|
|
76
|
+
} | undefined;
|
|
77
|
+
/** Allowed to throw; a throw reads as unreadable. */
|
|
78
|
+
readAccount?: (auth: {
|
|
79
|
+
apiUrl: string;
|
|
80
|
+
pat: string;
|
|
81
|
+
}) => Promise<AccountRead>;
|
|
82
|
+
local: () => Promise<Enablement> | Enablement;
|
|
83
|
+
/** Clears this machine's copy. Allowed to throw; see below. */
|
|
84
|
+
clearLocal: () => Promise<unknown>;
|
|
85
|
+
/** Bounds the account read from outside, so a backend that hangs cannot hang the caller. */
|
|
86
|
+
timeoutMs?: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The account-first read shared by the send gate, `vincentt dx` and `vincentt version`
|
|
90
|
+
* (f7 Scenario 3, 3c). The account decides when it answers; a "not granted" answer clears
|
|
91
|
+
* this machine's copy so a revoke made elsewhere reaches it. Only when the account cannot
|
|
92
|
+
* be read does the local flag stand on its own.
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolveConsentView(deps: ConsentViewDeps): Promise<ConsentView>;
|
|
95
|
+
/** `dxConsent` in `vincentt dx --json` (f7 Scenario 3c). */
|
|
96
|
+
export interface DxConsentJson {
|
|
97
|
+
state: "on" | "off" | "unknown";
|
|
98
|
+
checked: "account" | "machine";
|
|
99
|
+
at?: string;
|
|
100
|
+
via?: EnablementVia;
|
|
101
|
+
}
|
|
102
|
+
export declare function dxConsentJson(view: ConsentView): DxConsentJson;
|
|
53
103
|
/**
|
|
54
104
|
* Read enablement from the real machine config.
|
|
55
105
|
*
|
|
@@ -66,8 +116,10 @@ export declare function grantedOn(at: string): string;
|
|
|
66
116
|
/**
|
|
67
117
|
* The one visibility line, in the single pinned shape every human surface uses:
|
|
68
118
|
*
|
|
119
|
+
* Unattended send: ENABLED since 2026-04-01 (145 days) · granted from the CLI
|
|
69
120
|
* Unattended send: ENABLED since 2026-04-01 (145 days) · granted in the console
|
|
70
|
-
* Unattended send: ENABLED since 2026-04-01 (145 days) ·
|
|
121
|
+
* Unattended send: ENABLED since 2026-04-01 (145 days) · on your account
|
|
122
|
+
* Unattended send: ENABLED since 2026-04-01 (145 days) · this machine (account not checked)
|
|
71
123
|
*
|
|
72
124
|
* Returns undefined when not enabled. Nothing is printed in that case, on
|
|
73
125
|
* purpose: a line that prints unconditionally is a line readers learn to skip,
|
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/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
|
+
}
|