@vincentt-xr/harness 1.2.0 → 1.4.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 +4 -5
- package/dist/client/HarnessProvider.js +47 -16
- package/dist/client/annotate.d.ts +75 -6
- package/dist/client/annotate.js +428 -32
- package/dist/client/channel.d.ts +47 -0
- package/dist/client/channel.js +135 -0
- package/dist/client/consoleOrigin.d.ts +16 -0
- package/dist/client/consoleOrigin.js +39 -0
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.js +5 -1
- package/dist/client/previewOrigin.d.ts +50 -0
- package/dist/client/previewOrigin.js +76 -0
- package/dist/client/share.d.ts +15 -29
- package/dist/client/share.js +18 -43
- package/dist/shared/channel.d.ts +60 -0
- package/dist/shared/channel.js +45 -0
- package/dist/shared/events.d.ts +39 -0
- package/package.json +1 -1
|
@@ -24,11 +24,10 @@ export interface HarnessProviderProps {
|
|
|
24
24
|
captureNetwork?: boolean;
|
|
25
25
|
captureTrace?: boolean;
|
|
26
26
|
/**
|
|
27
|
-
* Mount the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* takes the adjacent flex slot with no layout move.
|
|
27
|
+
* Mount the Send-feedback chip (the reverse-channel capture overlay) into the
|
|
28
|
+
* shared top-right cluster. Defaults to `enabled` — on in preview, tree-shaken
|
|
29
|
+
* out of production. It is the phone's only way to speak, and f12's central
|
|
30
|
+
* scenario is unreachable without it.
|
|
32
31
|
*/
|
|
33
32
|
feedback?: boolean;
|
|
34
33
|
/**
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// but both are overridable for the production review-service path later.
|
|
10
10
|
import { useEffect } from "react";
|
|
11
11
|
import { CLIENT_SESSION_ID_RE } from "../shared/events.js";
|
|
12
|
+
import { isFramed } from "./previewOrigin.js";
|
|
12
13
|
function defaultRelayUrl() {
|
|
13
14
|
if (typeof window === "undefined")
|
|
14
15
|
return "ws://localhost:7331";
|
|
@@ -90,13 +91,53 @@ export function HarnessProvider(props) {
|
|
|
90
91
|
captureTrace: props.captureTrace,
|
|
91
92
|
});
|
|
92
93
|
});
|
|
94
|
+
let unmountShare;
|
|
93
95
|
let unmountFeedback;
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
// SHARE MOUNTS FIRST, AND THE ORDER IS SEQUENCED, NOT RACED.
|
|
97
|
+
//
|
|
98
|
+
// Insertion order into the cluster IS flex order, and Share is the cluster's
|
|
99
|
+
// established first member — a new control must take the adjacent slot
|
|
100
|
+
// without moving it. Two independent `import(...).then(...)` chains resolve
|
|
101
|
+
// in whatever order the loader finishes them, so leaving these parallel puts
|
|
102
|
+
// the cluster's layout at the mercy of module-fetch timing. Feedback is
|
|
103
|
+
// therefore mounted from INSIDE Share's continuation.
|
|
104
|
+
//
|
|
105
|
+
// Both remain separate dynamic imports, which is what lets a production
|
|
106
|
+
// build drop each overlay (and `qrcode`, reached only from share.js). The
|
|
107
|
+
// share import is also written FIRST in this file, so the ordering is legible
|
|
108
|
+
// to a reader (and to a source-level assertion) and not only to the runtime.
|
|
109
|
+
//
|
|
110
|
+
// Share defaults to `enabled`. Feedback defaults to `enabled` too (opt OUT
|
|
111
|
+
// with `feedback={false}`); the chip gates itself on the origin at mount, so
|
|
112
|
+
// the flag decides whether the module is fetched at all — as does the
|
|
113
|
+
// framing check hoisted into `mountFeedback` below.
|
|
114
|
+
if (props.share !== false) {
|
|
115
|
+
void import("./share.js").then(({ mountShareButton }) => {
|
|
116
|
+
if (cancelled)
|
|
117
|
+
return;
|
|
118
|
+
unmountShare = mountShareButton({});
|
|
119
|
+
// Only now — so Share holds slot one even when its module resolves last.
|
|
120
|
+
mountFeedback();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// No Share at all: feedback is the cluster's only member, so there is no
|
|
125
|
+
// order to preserve and nothing to wait for.
|
|
126
|
+
mountFeedback();
|
|
127
|
+
}
|
|
128
|
+
function mountFeedback() {
|
|
129
|
+
if (props.feedback === false)
|
|
130
|
+
return;
|
|
131
|
+
// THE FRAMING CHECK RUNS BEFORE THE IMPORT, not only inside the mount.
|
|
132
|
+
// `mountFeedbackButton` gates itself too (defence in depth), but that gate
|
|
133
|
+
// runs after the module has already been fetched and evaluated — so the
|
|
134
|
+
// annotation module, which carries an unauthenticated same-origin write
|
|
135
|
+
// path, was being loaded inside the framed document that now holds a
|
|
136
|
+
// camera delegation. Nothing was appended and no chip rendered, so there
|
|
137
|
+
// was no redress target; the code was simply present. Hoisting the check
|
|
138
|
+
// here closes that outright rather than mitigating it.
|
|
139
|
+
if (isFramed())
|
|
140
|
+
return;
|
|
100
141
|
void import("./annotate.js").then(({ mountFeedbackButton }) => {
|
|
101
142
|
if (cancelled)
|
|
102
143
|
return;
|
|
@@ -110,16 +151,6 @@ export function HarnessProvider(props) {
|
|
|
110
151
|
});
|
|
111
152
|
});
|
|
112
153
|
}
|
|
113
|
-
let unmountShare;
|
|
114
|
-
// Share defaults to `enabled`. Separate dynamic import so the overlay's DOM
|
|
115
|
-
// code and `qrcode` (reached only from here) drop from a production bundle.
|
|
116
|
-
if (props.share !== false) {
|
|
117
|
-
void import("./share.js").then(({ mountShareButton }) => {
|
|
118
|
-
if (cancelled)
|
|
119
|
-
return;
|
|
120
|
-
unmountShare = mountShareButton({});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
154
|
return () => {
|
|
124
155
|
cancelled = true;
|
|
125
156
|
teardown?.();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FramingView } from "./previewOrigin.js";
|
|
1
2
|
import type { AnnotationInput, AnnotationSpec } from "../shared/events.js";
|
|
2
3
|
/**
|
|
3
4
|
* Capture the current frame as a PNG data URL. Prefers the largest <canvas> (the
|
|
@@ -18,17 +19,85 @@ export declare function sendAnnotation(input: AnnotationInput, opts?: SendAnnota
|
|
|
18
19
|
id: string;
|
|
19
20
|
seq: number;
|
|
20
21
|
}>;
|
|
22
|
+
/** The counter's current value. Exported for tests; not part of the app surface. */
|
|
23
|
+
export declare function getConsecutiveFailures(): number;
|
|
24
|
+
/**
|
|
25
|
+
* ANY failed POST increments — non-2xx, network error, abort alike. The device
|
|
26
|
+
* cannot distinguish a dead tunnel from a dead relay from a dead preview, and
|
|
27
|
+
* must not try.
|
|
28
|
+
*/
|
|
29
|
+
export declare function recordSendFailure(): number;
|
|
30
|
+
/**
|
|
31
|
+
* A successful POST resets to 0, UNCONDITIONALLY. One send getting through proves
|
|
32
|
+
* the channel is alive, so the next failure is a first failure again.
|
|
33
|
+
*
|
|
34
|
+
* Nothing else calls this. Not the panel closing, not the chip being tapped, not
|
|
35
|
+
* a timer, not a visibility change — see the module comment.
|
|
36
|
+
*/
|
|
37
|
+
export declare function recordSendSuccess(): void;
|
|
38
|
+
/** Test-only: restore the module counter to its initial state. */
|
|
39
|
+
export declare function __resetFailureCounterForTests(): void;
|
|
40
|
+
/** At 2+ consecutive failures the channel is gone, not having a bad moment. */
|
|
41
|
+
export declare function isStranded(): boolean;
|
|
21
42
|
export interface FeedbackButtonOptions extends SendAnnotationOptions {
|
|
22
43
|
sessionId?: string;
|
|
23
|
-
/** Collect the creator's message. Default: window.prompt. */
|
|
24
|
-
promptMessage?: () => string | null | Promise<string | null>;
|
|
25
44
|
/** Override the captured spec (strokes/pins/labels). Default: empty. */
|
|
26
45
|
spec?: AnnotationSpec;
|
|
46
|
+
/** Injectable for tests. Defaults to `window.matchMedia`. */
|
|
47
|
+
matchMedia?: typeof window.matchMedia;
|
|
48
|
+
/** Injectable for tests. Defaults to `window.location`. */
|
|
49
|
+
location?: Pick<Location, "origin" | "protocol" | "hostname">;
|
|
50
|
+
/** Injectable for tests. Defaults to the real POST. */
|
|
51
|
+
send?: (input: AnnotationInput) => Promise<unknown>;
|
|
52
|
+
/** Injectable for tests. Defaults to `captureScreenshot`. */
|
|
53
|
+
capture?: () => string;
|
|
54
|
+
/** Injectable for tests. Defaults to the real `window` (self/top identity). */
|
|
55
|
+
view?: FramingView;
|
|
27
56
|
}
|
|
28
57
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
58
|
+
* Is the feedback chip shown? The MIRROR of Share's rule, because the two are
|
|
59
|
+
* opposite handoffs. Share is `!isPhone(mm) && isPreviewOrigin(loc)`; feedback
|
|
60
|
+
* keeps the origin arm and DROPS the phone veto, because a phone is the device
|
|
61
|
+
* this control exists for. So the gate is the origin arm alone:
|
|
62
|
+
*
|
|
63
|
+
* | context | Share | feedback |
|
|
64
|
+
* |-------------------------------|--------|----------|
|
|
65
|
+
* | coarse-pointer / narrow | absent | PRESENT |
|
|
66
|
+
* | desktop | present| present |
|
|
67
|
+
* | real preview origin | req'd | req'd |
|
|
68
|
+
* | plain localhost / LAN | absent | ABSENT — no relay reachable |
|
|
69
|
+
*
|
|
70
|
+
* `matchMedia` is NOT a parameter: no viewport class can change this answer, and
|
|
71
|
+
* taking one would imply a veto that does not exist. Never rendered disabled or
|
|
72
|
+
* explained-away — on localhost nothing is appended at all.
|
|
73
|
+
*/
|
|
74
|
+
export declare function shouldShowFeedback(loc: Pick<Location, "protocol" | "hostname">): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Mount the icon-only Send-feedback chip into the shared cluster and wire its
|
|
77
|
+
* composing panel.
|
|
78
|
+
*
|
|
79
|
+
* The show-gate is evaluated ONCE, here at mount. If it fails, nothing is
|
|
80
|
+
* appended to the DOM (no disabled state) and the returned unmount is a no-op.
|
|
81
|
+
*
|
|
82
|
+
* Share mounts first from HarnessProvider, and insertion order IS flex order, so
|
|
83
|
+
* appending here always leaves Share as the cluster's first member.
|
|
84
|
+
*
|
|
85
|
+
* THE FRAMED VETO (f13 `D-Feedback-does-not-mount-in-the-frame`, security
|
|
86
|
+
* MUST-FIX 5). Send POSTs to a route whose own comment states it is
|
|
87
|
+
* "unauthenticated and reachable by anyone holding the capability URL", and the
|
|
88
|
+
* write lands in the creator's coding-agent context. Inside a frame that control
|
|
89
|
+
* is positioned by the embedder's CSS on a page the creator did not choose to
|
|
90
|
+
* load it on, so a UI-redress lure (drag-and-paste into the textarea, a
|
|
91
|
+
* "click twice to continue" chain) reaches a text-injection channel needing no
|
|
92
|
+
* browser permission grant at all — the class of harm the frame's absent `allow`
|
|
93
|
+
* was built to stop, arriving through a door `allow` does not cover.
|
|
94
|
+
*
|
|
95
|
+
* THE MOUNT is gated, not the visibility. A hidden-but-mounted chip still holds
|
|
96
|
+
* the write path, and a hidden control is exactly what an overlay attack wants.
|
|
97
|
+
* Nothing is appended; there is no disabled state to re-enable.
|
|
98
|
+
*
|
|
99
|
+
* The cost is small and known: a creator viewing their own preview framed in
|
|
100
|
+
* their own console cannot Send from inside the frame. It is their own agent and
|
|
101
|
+
* the terminal is where they are already talking to it.
|
|
33
102
|
*/
|
|
34
103
|
export declare function mountFeedbackButton(opts?: FeedbackButtonOptions): () => void;
|