@vincentt-xr/harness 0.4.0 → 1.1.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/dist/client/HarnessProvider.d.ts +43 -1
- package/dist/client/HarnessProvider.js +66 -10
- package/dist/client/annotate.d.ts +34 -0
- package/dist/client/annotate.js +104 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +1 -0
- package/dist/shared/events.d.ts +138 -3
- package/dist/shared/events.js +25 -1
- package/dist/tunnel/cbor.d.ts +71 -0
- package/dist/tunnel/cbor.js +403 -0
- package/dist/tunnel/client.d.ts +219 -0
- package/dist/tunnel/client.js +620 -0
- package/dist/tunnel/forwardTarget.d.ts +69 -0
- package/dist/tunnel/forwardTarget.js +174 -0
- package/dist/tunnel/frame.d.ts +126 -0
- package/dist/tunnel/frame.js +244 -0
- package/dist/tunnel/index.d.ts +11 -0
- package/dist/tunnel/index.js +11 -0
- package/package.json +14 -31
- package/README.md +0 -87
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -55
- package/dist/login/login.d.ts +0 -34
- package/dist/login/login.js +0 -148
- package/dist/mcp/backend.d.ts +0 -52
- package/dist/mcp/backend.js +0 -146
- package/dist/mcp/cli.d.ts +0 -2
- package/dist/mcp/cli.js +0 -10
- package/dist/mcp/diagnostics.d.ts +0 -13
- package/dist/mcp/diagnostics.js +0 -61
- package/dist/mcp/server.d.ts +0 -16
- package/dist/mcp/server.js +0 -239
- package/dist/preview/cloudflared.d.ts +0 -13
- package/dist/preview/cloudflared.js +0 -46
- package/dist/preview/index.d.ts +0 -3
- package/dist/preview/index.js +0 -6
- package/dist/preview/net.d.ts +0 -6
- package/dist/preview/net.js +0 -56
- package/dist/preview/proxy.d.ts +0 -4
- package/dist/preview/proxy.js +0 -49
- package/dist/preview/runner.d.ts +0 -45
- package/dist/preview/runner.js +0 -110
- package/dist/preview/tunnel.d.ts +0 -14
- package/dist/preview/tunnel.js +0 -28
- package/dist/relay/cli.d.ts +0 -2
- package/dist/relay/cli.js +0 -7
- package/dist/relay/server.d.ts +0 -12
- package/dist/relay/server.js +0 -85
- package/dist/relay/store.d.ts +0 -13
- package/dist/relay/store.js +0 -68
- package/dist/scaffold/index.d.ts +0 -26
- package/dist/scaffold/index.js +0 -85
- package/dist/shared/config.d.ts +0 -39
- package/dist/shared/config.js +0 -90
|
@@ -12,10 +12,52 @@ export interface HarnessProviderProps {
|
|
|
12
12
|
* app). Override for the production review-service sink.
|
|
13
13
|
*/
|
|
14
14
|
relayUrl?: string;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* The key grouping this tab's events into one TESTER. Defaults to a per-tab
|
|
17
|
+
* CSPRNG id in `sessionStorage`, which is what you want; override only for the
|
|
18
|
+
* production review-service path. A value passed here is client-supplied input
|
|
19
|
+
* like any other — the relay validates it and never renders it, and passing
|
|
20
|
+
* another tab's value merges the two into one tester.
|
|
21
|
+
*/
|
|
16
22
|
sessionId?: string;
|
|
17
23
|
captureConsole?: boolean;
|
|
18
24
|
captureNetwork?: boolean;
|
|
19
25
|
captureTrace?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Mount the floating "Send feedback" button (the reverse-channel capture
|
|
28
|
+
* overlay). Defaults to `enabled` — on in preview, gone in production.
|
|
29
|
+
*/
|
|
30
|
+
feedback?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 128 bits of CSPRNG, hex, as `s2_<32 hex>`.
|
|
34
|
+
*
|
|
35
|
+
* There is deliberately NO Math.random fallback. The previous generator's ~10^6
|
|
36
|
+
* space is exactly what this replaces, and a fallback reintroducing it would be
|
|
37
|
+
* the branch that actually runs on the oldest phone in the room. Both paths here
|
|
38
|
+
* are Web Crypto: randomUUID where it exists, getRandomValues otherwise — and
|
|
39
|
+
* getRandomValues has been in every browser that can run this app for a decade.
|
|
40
|
+
* If neither exists we throw rather than guess, because a weak id is a SILENT
|
|
41
|
+
* merge of two testers' streams, and no id at all is merely un-attributed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function generateClientSessionId(): string;
|
|
44
|
+
/** The slice of Storage this needs — so the rule below is testable without a DOM. */
|
|
45
|
+
export interface SessionIdStorage {
|
|
46
|
+
getItem(key: string): string | null;
|
|
47
|
+
setItem(key: string, value: string): void;
|
|
20
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The key the relay groups this tab's events by — one TESTER.
|
|
51
|
+
*
|
|
52
|
+
* `sessionStorage`, not `localStorage`, and the choice is load-bearing on both
|
|
53
|
+
* ends: it is per TAB (so one QR scan is one tester even though the tab opens a
|
|
54
|
+
* second connection for assets) and it DIES WITH THE TAB (so the grouping is
|
|
55
|
+
* bounded by construction rather than by a rule someone has to remember, and
|
|
56
|
+
* cannot outlive the session's terminal window).
|
|
57
|
+
*
|
|
58
|
+
* A stored value that misses the current grammar is OVERWRITTEN, not migrated:
|
|
59
|
+
* it was minted at the entropy this replaces. The rewrite happens here at mount,
|
|
60
|
+
* before the first batch, so no id is ever renumbered mid-run.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveClientSessionId(storage: SessionIdStorage | undefined): string;
|
|
21
63
|
export declare function HarnessProvider(props: HarnessProviderProps): ReactNode;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// accident. The relay URL and session id default to what the preview loop sets,
|
|
9
9
|
// but both are overridable for the production review-service path later.
|
|
10
10
|
import { useEffect } from "react";
|
|
11
|
+
import { CLIENT_SESSION_ID_RE } from "../shared/events.js";
|
|
11
12
|
function defaultRelayUrl() {
|
|
12
13
|
if (typeof window === "undefined")
|
|
13
14
|
return "ws://localhost:7331";
|
|
@@ -16,19 +17,56 @@ function defaultRelayUrl() {
|
|
|
16
17
|
// so one cloudflared tunnel carries both. The relay CLI serves this path.
|
|
17
18
|
return `${proto}://${window.location.host}/__harness`;
|
|
18
19
|
}
|
|
20
|
+
const SESSION_STORAGE_KEY = "__vincentt_harness_session";
|
|
21
|
+
/**
|
|
22
|
+
* 128 bits of CSPRNG, hex, as `s2_<32 hex>`.
|
|
23
|
+
*
|
|
24
|
+
* There is deliberately NO Math.random fallback. The previous generator's ~10^6
|
|
25
|
+
* space is exactly what this replaces, and a fallback reintroducing it would be
|
|
26
|
+
* the branch that actually runs on the oldest phone in the room. Both paths here
|
|
27
|
+
* are Web Crypto: randomUUID where it exists, getRandomValues otherwise — and
|
|
28
|
+
* getRandomValues has been in every browser that can run this app for a decade.
|
|
29
|
+
* If neither exists we throw rather than guess, because a weak id is a SILENT
|
|
30
|
+
* merge of two testers' streams, and no id at all is merely un-attributed.
|
|
31
|
+
*/
|
|
32
|
+
export function generateClientSessionId() {
|
|
33
|
+
const c = globalThis.crypto;
|
|
34
|
+
if (c?.randomUUID)
|
|
35
|
+
return `s2_${c.randomUUID().replace(/-/g, "")}`;
|
|
36
|
+
if (c?.getRandomValues) {
|
|
37
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
38
|
+
let hex = "";
|
|
39
|
+
for (const b of bytes)
|
|
40
|
+
hex += b.toString(16).padStart(2, "0");
|
|
41
|
+
return `s2_${hex}`;
|
|
42
|
+
}
|
|
43
|
+
throw new Error("harness: Web Crypto unavailable; cannot mint a session id");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The key the relay groups this tab's events by — one TESTER.
|
|
47
|
+
*
|
|
48
|
+
* `sessionStorage`, not `localStorage`, and the choice is load-bearing on both
|
|
49
|
+
* ends: it is per TAB (so one QR scan is one tester even though the tab opens a
|
|
50
|
+
* second connection for assets) and it DIES WITH THE TAB (so the grouping is
|
|
51
|
+
* bounded by construction rather than by a rule someone has to remember, and
|
|
52
|
+
* cannot outlive the session's terminal window).
|
|
53
|
+
*
|
|
54
|
+
* A stored value that misses the current grammar is OVERWRITTEN, not migrated:
|
|
55
|
+
* it was minted at the entropy this replaces. The rewrite happens here at mount,
|
|
56
|
+
* before the first batch, so no id is ever renumbered mid-run.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveClientSessionId(storage) {
|
|
59
|
+
const existing = storage?.getItem(SESSION_STORAGE_KEY);
|
|
60
|
+
if (existing && CLIENT_SESSION_ID_RE.test(existing))
|
|
61
|
+
return existing;
|
|
62
|
+
const id = generateClientSessionId();
|
|
63
|
+
storage?.setItem(SESSION_STORAGE_KEY, id);
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
19
66
|
function defaultSessionId() {
|
|
20
67
|
if (typeof window === "undefined")
|
|
21
68
|
return "server";
|
|
22
|
-
|
|
23
|
-
const existing = window.sessionStorage?.getItem(key);
|
|
24
|
-
if (existing)
|
|
25
|
-
return existing;
|
|
26
|
-
// Per-load id; not security-sensitive, just needs to be distinct enough that
|
|
27
|
-
// two phones on one relay don't collide. No Math.random dependency in the
|
|
28
|
-
// pure layer — this imperative shell may use it.
|
|
29
|
-
const id = `s_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`;
|
|
30
|
-
window.sessionStorage?.setItem(key, id);
|
|
31
|
-
return id;
|
|
69
|
+
return resolveClientSessionId(window.sessionStorage);
|
|
32
70
|
}
|
|
33
71
|
const DEV = typeof import.meta !== "undefined" &&
|
|
34
72
|
import.meta.env?.DEV;
|
|
@@ -52,9 +90,27 @@ export function HarnessProvider(props) {
|
|
|
52
90
|
captureTrace: props.captureTrace,
|
|
53
91
|
});
|
|
54
92
|
});
|
|
93
|
+
let unmountFeedback;
|
|
94
|
+
if (props.feedback !== false) {
|
|
95
|
+
// Separate dynamic import so the capture overlay (and its DOM code) also
|
|
96
|
+
// drops from a production bundle.
|
|
97
|
+
void import("./annotate.js").then(({ mountFeedbackButton }) => {
|
|
98
|
+
if (cancelled)
|
|
99
|
+
return;
|
|
100
|
+
// The SAME resolved key the instrumentation sends, not the bare prop.
|
|
101
|
+
// The prop is undefined for every app that does not set it — i.e. the
|
|
102
|
+
// default — so passing it through left every real annotation
|
|
103
|
+
// un-attributed while the tests, which build the input by hand, never
|
|
104
|
+
// exercised this line.
|
|
105
|
+
unmountFeedback = mountFeedbackButton({
|
|
106
|
+
sessionId: props.sessionId ?? defaultSessionId(),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
55
110
|
return () => {
|
|
56
111
|
cancelled = true;
|
|
57
112
|
teardown?.();
|
|
113
|
+
unmountFeedback?.();
|
|
58
114
|
};
|
|
59
115
|
// Instrumentation is installed once for the provider's lifetime; option
|
|
60
116
|
// changes mid-session are not a supported case.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AnnotationInput, AnnotationSpec } from "../shared/events.js";
|
|
2
|
+
/**
|
|
3
|
+
* Capture the current frame as a PNG data URL. Prefers the largest <canvas> (the
|
|
4
|
+
* AR/three.js surface); returns an empty-ish 1x1 PNG if there is none.
|
|
5
|
+
*
|
|
6
|
+
* Caveat: reading a WebGL canvas requires it to have been created with
|
|
7
|
+
* `preserveDrawingBuffer: true`, or the read can come back blank. r3f/three set
|
|
8
|
+
* this via `gl={{ preserveDrawingBuffer: true }}`. DOM-only content isn't captured
|
|
9
|
+
* here (that needs html2canvas, deliberately not a dependency).
|
|
10
|
+
*/
|
|
11
|
+
export declare function captureScreenshot(): string;
|
|
12
|
+
export interface SendAnnotationOptions {
|
|
13
|
+
/** Relay HTTP base (default: `<origin>/__harness`). */
|
|
14
|
+
relayHttpUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
/** POST an annotation to the relay. Resolves with the relay-assigned id + seq. */
|
|
17
|
+
export declare function sendAnnotation(input: AnnotationInput, opts?: SendAnnotationOptions): Promise<{
|
|
18
|
+
id: string;
|
|
19
|
+
seq: number;
|
|
20
|
+
}>;
|
|
21
|
+
export interface FeedbackButtonOptions extends SendAnnotationOptions {
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
/** Collect the creator's message. Default: window.prompt. */
|
|
24
|
+
promptMessage?: () => string | null | Promise<string | null>;
|
|
25
|
+
/** Override the captured spec (strokes/pins/labels). Default: empty. */
|
|
26
|
+
spec?: AnnotationSpec;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Mount a minimal floating "Send feedback" button. On click it collects a message,
|
|
30
|
+
* captures the frame, and sends the annotation. Returns an unmount function. This
|
|
31
|
+
* is the smallest useful Send action; a richer draw-on-frame overlay can replace
|
|
32
|
+
* the capture/prompt without changing the wire contract.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mountFeedbackButton(opts?: FeedbackButtonOptions): () => void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The in-app capture overlay's Send action — the client half of the reverse
|
|
2
|
+
// channel. It captures the current frame, packages the creator's message + spec,
|
|
3
|
+
// and POSTs it to the relay over the same tunnel the app is served on. The relay
|
|
4
|
+
// (see the CLI's relay/annotations) stamps + persists it and unblocks a waiting
|
|
5
|
+
// `vincentt feedback --wait`.
|
|
6
|
+
//
|
|
7
|
+
// Loaded via dynamic import from HarnessProvider (like the instrumentation), so a
|
|
8
|
+
// production build never ships it. DOM-only, no React.
|
|
9
|
+
/** Default relay HTTP base: the same origin the app is served on + the harness path. */
|
|
10
|
+
function defaultRelayHttpUrl() {
|
|
11
|
+
return `${window.location.origin}/__harness`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Capture the current frame as a PNG data URL. Prefers the largest <canvas> (the
|
|
15
|
+
* AR/three.js surface); returns an empty-ish 1x1 PNG if there is none.
|
|
16
|
+
*
|
|
17
|
+
* Caveat: reading a WebGL canvas requires it to have been created with
|
|
18
|
+
* `preserveDrawingBuffer: true`, or the read can come back blank. r3f/three set
|
|
19
|
+
* this via `gl={{ preserveDrawingBuffer: true }}`. DOM-only content isn't captured
|
|
20
|
+
* here (that needs html2canvas, deliberately not a dependency).
|
|
21
|
+
*/
|
|
22
|
+
export function captureScreenshot() {
|
|
23
|
+
const canvases = Array.from(document.querySelectorAll("canvas"));
|
|
24
|
+
const biggest = canvases.sort((a, b) => b.width * b.height - a.width * a.height)[0];
|
|
25
|
+
if (biggest) {
|
|
26
|
+
try {
|
|
27
|
+
return biggest.toDataURL("image/png");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Tainted canvas (cross-origin texture without CORS) — fall through.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const fallback = document.createElement("canvas");
|
|
34
|
+
fallback.width = fallback.height = 1;
|
|
35
|
+
return fallback.toDataURL("image/png");
|
|
36
|
+
}
|
|
37
|
+
/** POST an annotation to the relay. Resolves with the relay-assigned id + seq. */
|
|
38
|
+
export async function sendAnnotation(input, opts = {}) {
|
|
39
|
+
const base = opts.relayHttpUrl ?? defaultRelayHttpUrl();
|
|
40
|
+
const res = await fetch(`${base}/annotation`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "content-type": "application/json" },
|
|
43
|
+
body: JSON.stringify(input),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok)
|
|
46
|
+
throw new Error(`annotation POST failed: ${res.status}`);
|
|
47
|
+
return (await res.json());
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mount a minimal floating "Send feedback" button. On click it collects a message,
|
|
51
|
+
* captures the frame, and sends the annotation. Returns an unmount function. This
|
|
52
|
+
* is the smallest useful Send action; a richer draw-on-frame overlay can replace
|
|
53
|
+
* the capture/prompt without changing the wire contract.
|
|
54
|
+
*/
|
|
55
|
+
export function mountFeedbackButton(opts = {}) {
|
|
56
|
+
if (typeof document === "undefined")
|
|
57
|
+
return () => undefined;
|
|
58
|
+
const btn = document.createElement("button");
|
|
59
|
+
btn.textContent = "Send feedback";
|
|
60
|
+
Object.assign(btn.style, {
|
|
61
|
+
position: "fixed",
|
|
62
|
+
right: "12px",
|
|
63
|
+
bottom: "12px",
|
|
64
|
+
zIndex: "2147483647",
|
|
65
|
+
padding: "10px 14px",
|
|
66
|
+
borderRadius: "10px",
|
|
67
|
+
border: "none",
|
|
68
|
+
background: "linear-gradient(135deg,#7c3aed,#2563eb)",
|
|
69
|
+
color: "#fff",
|
|
70
|
+
font: "600 13px/1 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif",
|
|
71
|
+
boxShadow: "0 4px 16px rgba(0,0,0,.25)",
|
|
72
|
+
cursor: "pointer",
|
|
73
|
+
});
|
|
74
|
+
const setBusy = (busy, label) => {
|
|
75
|
+
btn.disabled = busy;
|
|
76
|
+
btn.textContent = label ?? "Send feedback";
|
|
77
|
+
btn.style.opacity = busy ? "0.6" : "1";
|
|
78
|
+
};
|
|
79
|
+
btn.addEventListener("click", async () => {
|
|
80
|
+
const getMsg = opts.promptMessage ?? (() => window.prompt("Feedback for the agent:"));
|
|
81
|
+
const message = await getMsg();
|
|
82
|
+
if (message == null || message.trim() === "")
|
|
83
|
+
return;
|
|
84
|
+
setBusy(true, "Sending…");
|
|
85
|
+
try {
|
|
86
|
+
const screenshot = captureScreenshot();
|
|
87
|
+
const input = {
|
|
88
|
+
message: message.trim(),
|
|
89
|
+
screenshot,
|
|
90
|
+
spec: opts.spec ?? {},
|
|
91
|
+
sessionId: opts.sessionId,
|
|
92
|
+
};
|
|
93
|
+
await sendAnnotation(input, { relayHttpUrl: opts.relayHttpUrl });
|
|
94
|
+
setBusy(false, "Sent ✓");
|
|
95
|
+
window.setTimeout(() => setBusy(false), 1500);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
setBusy(false, "Failed — retry");
|
|
99
|
+
console.error("[harness] annotation send failed:", err);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
document.body.appendChild(btn);
|
|
103
|
+
return () => btn.remove();
|
|
104
|
+
}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { HarnessProvider } from "./HarnessProvider.js";
|
|
2
2
|
export type { HarnessProviderProps } from "./HarnessProvider.js";
|
|
3
|
+
export { sendAnnotation, captureScreenshot, mountFeedbackButton, type SendAnnotationOptions, type FeedbackButtonOptions, } from "./annotate.js";
|
|
3
4
|
export type { DiagEvent, LogEvent, NetworkEvent, TraceEvent } from "../shared/events.js";
|
|
5
|
+
export type { Annotation, AnnotationInput, AnnotationSpec, AnnotationStroke, AnnotationPin, } from "../shared/events.js";
|
package/dist/client/index.js
CHANGED
package/dist/shared/events.d.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
/** Monotonic-ish wall-clock ms since epoch, stamped by the client at capture. */
|
|
2
2
|
export type Timestamp = number;
|
|
3
|
+
/**
|
|
4
|
+
* The grammar of the client's session id — the key the relay groups a TESTER by.
|
|
5
|
+
*
|
|
6
|
+
* It lives here, in the file all three parts import, for the same reason
|
|
7
|
+
* VIEWER_LABEL_RE lives beside the codec that produces the label: the party that
|
|
8
|
+
* mints the value and the party that validates it must not be able to disagree
|
|
9
|
+
* about its shape. The client writes this shape; the relay accepts this shape and
|
|
10
|
+
* nothing else, with no sanitize-and-keep branch.
|
|
11
|
+
*
|
|
12
|
+
* `s2_` is generation two. Generation one was `s_<base36 time>_<base36 rand>` over
|
|
13
|
+
* ~10^6 of Math.random, which grouped a RUN. Grouping a TESTER makes a collision a
|
|
14
|
+
* silent merge of two people's streams, so the space is now 128 bits and the
|
|
15
|
+
* prefix is what lets a relay tell the two generations apart on sight.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CLIENT_SESSION_ID_RE: RegExp;
|
|
18
|
+
/**
|
|
19
|
+
* A relay-assigned tester id: `t1`, `t2`, … Minted on the CREATOR'S machine and
|
|
20
|
+
* never on the wire from the device — it is a local relabeling of a key the
|
|
21
|
+
* device supplied, which is what keeps the platform free of a correlator (B22).
|
|
22
|
+
*
|
|
23
|
+
* No leading zero. Ids are counted from 1 and never zero-padded, so `t01` is a
|
|
24
|
+
* string the registry cannot have produced; accepting it at the filter boundary
|
|
25
|
+
* would admit a second spelling for a value with one canonical form.
|
|
26
|
+
*/
|
|
27
|
+
export declare const TESTER_ID_RE: RegExp;
|
|
3
28
|
/** Kinds of thing the diagnostics limb observes. */
|
|
4
29
|
export type DiagEventKind = "log" | "network" | "trace";
|
|
5
30
|
interface DiagEventBase {
|
|
@@ -62,17 +87,127 @@ export interface ClientToRelay {
|
|
|
62
87
|
}
|
|
63
88
|
/** What the MCP server asks the relay for. `since` is an exclusive seq cursor. */
|
|
64
89
|
export interface RelayQuery {
|
|
65
|
-
sessionId?: string;
|
|
66
90
|
kind?: DiagEventKind;
|
|
67
91
|
since?: number;
|
|
68
92
|
/** Cap the returned count (newest-biased); relay clamps to its buffer size. */
|
|
69
93
|
limit?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Filter to one TESTER — one browser tab, recognised across refreshes. This is
|
|
96
|
+
* the filter that answers "it breaks on their phone but not mine", because it
|
|
97
|
+
* survives the reload the tester does first.
|
|
98
|
+
*
|
|
99
|
+
* A malformed id filters to NOTHING rather than being ignored: silently
|
|
100
|
+
* widening a filter shows the creator several testers' events under a heading
|
|
101
|
+
* naming one.
|
|
102
|
+
*/
|
|
103
|
+
tester?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Filter to one viewer — one CONNECTION (2.8), not an identity (B22). The
|
|
106
|
+
* sub-coordinate below the tester: a single tab opens several of these under
|
|
107
|
+
* HTTP/1.1, so this narrows to one of them and is for connection-level
|
|
108
|
+
* debugging rather than for "whose phone".
|
|
109
|
+
*/
|
|
110
|
+
viewer?: string;
|
|
70
111
|
}
|
|
71
112
|
export interface RelayResult {
|
|
72
113
|
events: DiagEvent[];
|
|
114
|
+
/**
|
|
115
|
+
* `eventViewers[i]` is the label that produced `events[i]`, or undefined when
|
|
116
|
+
* the relay could not attribute it.
|
|
117
|
+
*
|
|
118
|
+
* It rides ALONGSIDE the events rather than on them because a viewer label is
|
|
119
|
+
* a fact about the CONNECTION that carried an observation, not a property of
|
|
120
|
+
* the observation — putting it on the event would make it look like something
|
|
121
|
+
* the device reported about itself, which is the identity promotion B22
|
|
122
|
+
* forbids.
|
|
123
|
+
*/
|
|
124
|
+
eventViewers?: (string | undefined)[];
|
|
125
|
+
/**
|
|
126
|
+
* `eventTesters[i]` is the tester that produced `events[i]`, or undefined when
|
|
127
|
+
* the relay could not attribute it (the pre-mount window, a crashed app, a
|
|
128
|
+
* non-app request, or a session past the cap).
|
|
129
|
+
*
|
|
130
|
+
* Rides alongside for the same reason as `eventViewers`: which tester an
|
|
131
|
+
* observation came from is a fact about the CONNECTION that carried it, not a
|
|
132
|
+
* property of the observation.
|
|
133
|
+
*/
|
|
134
|
+
eventTesters?: (string | undefined)[];
|
|
73
135
|
/** Highest seq the relay currently holds, so the caller can advance `since`. */
|
|
74
136
|
latestSeq: number;
|
|
75
|
-
/**
|
|
76
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Tester ids the relay has assigned, in arrival order. Every member matches
|
|
139
|
+
* TESTER_ID_RE.
|
|
140
|
+
*
|
|
141
|
+
* This REPLACED a `sessions` array that carried the raw client-supplied key to
|
|
142
|
+
* the terminal and to MCP tool output. That was a viewer-controlled string
|
|
143
|
+
* with no validation reaching the creator's coding agent — do not reintroduce
|
|
144
|
+
* it. The raw key is an opaque map key inside the relay and nothing else.
|
|
145
|
+
*/
|
|
146
|
+
testers: string[];
|
|
147
|
+
/**
|
|
148
|
+
* Viewer labels the relay has seen. Labels churn — a reconnecting device gets
|
|
149
|
+
* a NEW label and the old one is never reused — so the caller needs the live
|
|
150
|
+
* set rather than remembering one.
|
|
151
|
+
*/
|
|
152
|
+
viewers: string[];
|
|
153
|
+
}
|
|
154
|
+
/** A freehand stroke over the frame, in normalized [0,1] frame coordinates. */
|
|
155
|
+
export interface AnnotationStroke {
|
|
156
|
+
points: {
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
}[];
|
|
160
|
+
color?: string;
|
|
161
|
+
}
|
|
162
|
+
/** A point marker on the frame, in normalized [0,1] frame coordinates. */
|
|
163
|
+
export interface AnnotationPin {
|
|
164
|
+
x: number;
|
|
165
|
+
y: number;
|
|
166
|
+
label?: string;
|
|
167
|
+
}
|
|
168
|
+
/** The drawn overlay accompanying an annotation. All fields optional — a bare
|
|
169
|
+
* message with a screenshot is a valid annotation. */
|
|
170
|
+
export interface AnnotationSpec {
|
|
171
|
+
strokes?: AnnotationStroke[];
|
|
172
|
+
pins?: AnnotationPin[];
|
|
173
|
+
labels?: string[];
|
|
174
|
+
note?: string;
|
|
175
|
+
}
|
|
176
|
+
/** Client → relay POST body (`POST /__harness/annotation`). The relay stamps the
|
|
177
|
+
* id, receipt time, project binding, and on-disk screenshot path. */
|
|
178
|
+
export interface AnnotationInput {
|
|
179
|
+
/** The creator's message to the agent. */
|
|
180
|
+
message: string;
|
|
181
|
+
/** Annotated frame as a data URL (image/png). */
|
|
182
|
+
screenshot: string;
|
|
183
|
+
spec?: AnnotationSpec;
|
|
184
|
+
/**
|
|
185
|
+
* The client's session key, so the annotation can be correlated with the diag
|
|
186
|
+
* events from the same tab. Client-supplied and therefore hostile input: the
|
|
187
|
+
* relay REPLACES it with the tester id it resolves to, or drops it. It is
|
|
188
|
+
* never persisted or forwarded verbatim — this record reaches an agent.
|
|
189
|
+
*/
|
|
190
|
+
sessionId?: string;
|
|
191
|
+
}
|
|
192
|
+
/** A persisted annotation the agent consumes (via `vincentt feedback --wait`). */
|
|
193
|
+
export interface Annotation {
|
|
194
|
+
/** Short relay-assigned id (e.g. "a7f3"). */
|
|
195
|
+
id: string;
|
|
196
|
+
/** Monotonic per-project cursor; a consumer polls "after this seq". */
|
|
197
|
+
seq: number;
|
|
198
|
+
/** Receipt time, stamped by the relay (not the browser). */
|
|
199
|
+
createdAt: Timestamp;
|
|
200
|
+
/** The bound project directory this annotation belongs to. */
|
|
201
|
+
projectDir: string;
|
|
202
|
+
/** The creator's message to the agent. */
|
|
203
|
+
message: string;
|
|
204
|
+
/** Absolute path to the full-res annotated screenshot on disk. */
|
|
205
|
+
screenshotPath: string;
|
|
206
|
+
spec: AnnotationSpec;
|
|
207
|
+
/**
|
|
208
|
+
* The TESTER this annotation came from, or absent when the relay could not
|
|
209
|
+
* attribute it. Never the raw client key — see AnnotationInput.sessionId.
|
|
210
|
+
*/
|
|
211
|
+
sessionId?: string;
|
|
77
212
|
}
|
|
78
213
|
export {};
|
package/dist/shared/events.js
CHANGED
|
@@ -3,4 +3,28 @@
|
|
|
3
3
|
// server (which serves them to the agent). All three import THIS file, so the
|
|
4
4
|
// three can never drift on the shape of an event. Nothing here imports React,
|
|
5
5
|
// node, or MCP — it is pure data.
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The grammar of the client's session id — the key the relay groups a TESTER by.
|
|
8
|
+
*
|
|
9
|
+
* It lives here, in the file all three parts import, for the same reason
|
|
10
|
+
* VIEWER_LABEL_RE lives beside the codec that produces the label: the party that
|
|
11
|
+
* mints the value and the party that validates it must not be able to disagree
|
|
12
|
+
* about its shape. The client writes this shape; the relay accepts this shape and
|
|
13
|
+
* nothing else, with no sanitize-and-keep branch.
|
|
14
|
+
*
|
|
15
|
+
* `s2_` is generation two. Generation one was `s_<base36 time>_<base36 rand>` over
|
|
16
|
+
* ~10^6 of Math.random, which grouped a RUN. Grouping a TESTER makes a collision a
|
|
17
|
+
* silent merge of two people's streams, so the space is now 128 bits and the
|
|
18
|
+
* prefix is what lets a relay tell the two generations apart on sight.
|
|
19
|
+
*/
|
|
20
|
+
export const CLIENT_SESSION_ID_RE = /^s2_[0-9a-f]{32}$/;
|
|
21
|
+
/**
|
|
22
|
+
* A relay-assigned tester id: `t1`, `t2`, … Minted on the CREATOR'S machine and
|
|
23
|
+
* never on the wire from the device — it is a local relabeling of a key the
|
|
24
|
+
* device supplied, which is what keeps the platform free of a correlator (B22).
|
|
25
|
+
*
|
|
26
|
+
* No leading zero. Ids are counted from 1 and never zero-padded, so `t01` is a
|
|
27
|
+
* string the registry cannot have produced; accepting it at the filter boundary
|
|
28
|
+
* would admit a second spelling for a value with one canonical form.
|
|
29
|
+
*/
|
|
30
|
+
export const TESTER_ID_RE = /^t[1-9][0-9]{0,2}$/;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CBOR SUBSET, Node half. It mirrors `api/internal/edge/cbor.go` decision
|
|
3
|
+
* for decision, because two implementations of one wire format that were
|
|
4
|
+
* written independently are two implementations that drift.
|
|
5
|
+
*
|
|
6
|
+
* It decodes CONTROL payloads only — OPEN and HEAD. It NEVER touches a DATA
|
|
7
|
+
* payload.
|
|
8
|
+
*
|
|
9
|
+
* It is hand-written rather than a library for the same reason the Go one is: a
|
|
10
|
+
* general CBOR library's allocation behavior on hostile input would be someone
|
|
11
|
+
* else's decision, and the limits below are the whole point.
|
|
12
|
+
*
|
|
13
|
+
* Everything indefinite-length, every tag, every float, every byte string and
|
|
14
|
+
* every negative integer is REFUSED — not because they are dangerous in
|
|
15
|
+
* themselves, but because the encoder we own never emits them, so accepting
|
|
16
|
+
* them would only widen the input this parser has to be correct about.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* VIEWER_LABEL_RE is the SAME regex the edge assigns against, and this client
|
|
20
|
+
* RE-VALIDATES rather than trusting the edge. That is closure (c) of the three
|
|
21
|
+
* that keep viewer-supplied bytes out of the creator's agent's context: the
|
|
22
|
+
* label flows viewerLabel -> the relay ring-buffer key -> CLI terminal output
|
|
23
|
+
* and MCP tool output, which is a terminal/log-injection path INTO the agent,
|
|
24
|
+
* and a Go-only check would pass on a client that trusted the edge.
|
|
25
|
+
*/
|
|
26
|
+
export declare const VIEWER_LABEL_RE: RegExp;
|
|
27
|
+
export declare function parseViewerLabel(s: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* UA_CLASSES is the CLOSED ENUM OF SIX, mirroring the Go edge. `unknown` is the
|
|
30
|
+
* TOTAL fallback. This client validates the class it receives against this set
|
|
31
|
+
* before stamping the relay handshake, so an unmatched or forged class becomes
|
|
32
|
+
* `unknown` rather than an echo.
|
|
33
|
+
*/
|
|
34
|
+
export declare const UA_CLASSES: readonly ["ios_safari", "android_chrome", "desktop_chrome", "desktop_safari", "desktop_firefox", "unknown"];
|
|
35
|
+
export type UAClass = (typeof UA_CLASSES)[number];
|
|
36
|
+
export declare function parseUAClass(s: string): UAClass;
|
|
37
|
+
export interface OpenPayload {
|
|
38
|
+
viewerLabel: string;
|
|
39
|
+
method: string;
|
|
40
|
+
path: string;
|
|
41
|
+
headers: [string, string][];
|
|
42
|
+
upgrade?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface HeadPayload {
|
|
45
|
+
status: number;
|
|
46
|
+
headers: [string, string][];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `viewer_gone` is the ONE non-terminal kind: the other three mean the session
|
|
50
|
+
* is ending, this one means a viewer's connection closed while the session
|
|
51
|
+
* continues. It is also the only kind carrying a second key.
|
|
52
|
+
*/
|
|
53
|
+
export type ControlKind = "revoke" | "expiring" | "superseded" | "viewer_gone";
|
|
54
|
+
/** A decoded CONTROL payload. `viewerLabel` is present only on `viewer_gone`. */
|
|
55
|
+
export interface ControlPayload {
|
|
56
|
+
kind: ControlKind;
|
|
57
|
+
viewerLabel?: string;
|
|
58
|
+
}
|
|
59
|
+
export declare function decodeOpen(b: Uint8Array): OpenPayload;
|
|
60
|
+
export declare function decodeHead(b: Uint8Array): HeadPayload;
|
|
61
|
+
/**
|
|
62
|
+
* The map arity is asserted AGAINST THE KIND, so a payload claiming `revoke`
|
|
63
|
+
* with a trailing label is refused as malformed rather than quietly accepted
|
|
64
|
+
* with the extra key ignored.
|
|
65
|
+
*/
|
|
66
|
+
export declare function decodeControl(b: Uint8Array): ControlPayload;
|
|
67
|
+
/** Canonical key order: viewerLabel, method, path, headers, [upgrade]. */
|
|
68
|
+
export declare function encodeOpen(p: OpenPayload): Uint8Array;
|
|
69
|
+
/** Canonical key order: status, headers. */
|
|
70
|
+
export declare function encodeHead(p: HeadPayload): Uint8Array;
|
|
71
|
+
export declare function encodeControl(kind: ControlKind, viewerLabel?: string): Uint8Array;
|