@sailfish-ai/recorder 1.12.5 → 1.12.7
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/README.md +8 -0
- package/dist/canvas/adapters.js +382 -0
- package/dist/canvas/adapters.js.br +0 -0
- package/dist/canvas/adapters.js.gz +0 -0
- package/dist/canvas/canvasManager.js +1949 -0
- package/dist/canvas/canvasManager.js.br +0 -0
- package/dist/canvas/canvasManager.js.gz +0 -0
- package/dist/canvas/canvasPrivacy.js +233 -0
- package/dist/canvas/canvasPrivacy.js.br +0 -0
- package/dist/canvas/canvasPrivacy.js.gz +0 -0
- package/dist/canvas/codec.js +314 -0
- package/dist/canvas/codec.js.br +0 -0
- package/dist/canvas/codec.js.gz +0 -0
- package/dist/canvas/deviceClass.js +67 -0
- package/dist/canvas/deviceClass.js.br +0 -0
- package/dist/canvas/deviceClass.js.gz +0 -0
- package/dist/canvas/encodeWorker.js +284 -0
- package/dist/canvas/encodeWorker.js.br +0 -0
- package/dist/canvas/encodeWorker.js.gz +0 -0
- package/dist/canvas/governor.js +79 -0
- package/dist/canvas/governor.js.br +0 -0
- package/dist/canvas/governor.js.gz +0 -0
- package/dist/canvas/keyframes.js +183 -0
- package/dist/canvas/keyframes.js.br +0 -0
- package/dist/canvas/keyframes.js.gz +0 -0
- package/dist/canvas/mediaRecorderCapture.js +257 -0
- package/dist/canvas/mediaRecorderCapture.js.br +0 -0
- package/dist/canvas/mediaRecorderCapture.js.gz +0 -0
- package/dist/canvas/objectLog.js +150 -0
- package/dist/canvas/objectLog.js.br +0 -0
- package/dist/canvas/objectLog.js.gz +0 -0
- package/dist/canvas/patch2d.js +1302 -0
- package/dist/canvas/patch2d.js.br +0 -0
- package/dist/canvas/patch2d.js.gz +0 -0
- package/dist/canvas/patchGL.js +371 -0
- package/dist/canvas/patchGL.js.br +0 -0
- package/dist/canvas/patchGL.js.gz +0 -0
- package/dist/canvas/patchWebGPU.js +361 -0
- package/dist/canvas/patchWebGPU.js.br +0 -0
- package/dist/canvas/patchWebGPU.js.gz +0 -0
- package/dist/canvas/piiDetect.js +155 -0
- package/dist/canvas/piiDetect.js.br +0 -0
- package/dist/canvas/piiDetect.js.gz +0 -0
- package/dist/canvas/protocol.js +122 -0
- package/dist/canvas/protocol.js.br +0 -0
- package/dist/canvas/protocol.js.gz +0 -0
- package/dist/canvas/videoCapture.js +252 -0
- package/dist/canvas/videoCapture.js.br +0 -0
- package/dist/canvas/videoCapture.js.gz +0 -0
- package/dist/canvas/workerRelay.js +99 -0
- package/dist/canvas/workerRelay.js.br +0 -0
- package/dist/canvas/workerRelay.js.gz +0 -0
- package/dist/canvas/workerShim.js +227 -0
- package/dist/canvas/workerShim.js.br +0 -0
- package/dist/canvas/workerShim.js.gz +0 -0
- package/dist/canvas-worker.cjs +74 -0
- package/dist/canvas-worker.cjs.br +0 -0
- package/dist/canvas-worker.cjs.gz +0 -0
- package/dist/canvas-worker.js +78 -0
- package/dist/canvas-worker.js.br +0 -0
- package/dist/canvas-worker.js.gz +0 -0
- package/dist/canvasFrameStore.js +198 -0
- package/dist/canvasFrameStore.js.br +0 -0
- package/dist/canvasFrameStore.js.gz +0 -0
- package/dist/chunks/canvasManager-B9uQ-PFS.js +1456 -0
- package/dist/chunks/canvasManager-B9uQ-PFS.js.br +0 -0
- package/dist/chunks/canvasManager-B9uQ-PFS.js.gz +0 -0
- package/dist/chunks/canvasManager-Ce7vazTt.js +1440 -0
- package/dist/chunks/canvasManager-Ce7vazTt.js.br +0 -0
- package/dist/chunks/canvasManager-Ce7vazTt.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-pkpgT-FH.js → chunkSerializer-B-z7p0r-.js} +1 -1
- package/dist/chunks/chunkSerializer-B-z7p0r-.js.br +0 -0
- package/dist/chunks/chunkSerializer-B-z7p0r-.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-Cd8y9tID.js → chunkSerializer-C1knjCQ5.js} +1 -1
- package/dist/chunks/chunkSerializer-C1knjCQ5.js.br +0 -0
- package/dist/chunks/chunkSerializer-C1knjCQ5.js.gz +0 -0
- package/dist/chunks/{index-CXvHbLRT.js → index-BMHQ-0NY.js} +1115 -724
- package/dist/chunks/index-BMHQ-0NY.js.br +0 -0
- package/dist/chunks/index-BMHQ-0NY.js.gz +0 -0
- package/dist/chunks/{index-Bvd8oTpV.js → index-qgM1hFXO.js} +1025 -655
- package/dist/chunks/index-qgM1hFXO.js.br +0 -0
- package/dist/chunks/index-qgM1hFXO.js.gz +0 -0
- package/dist/chunks/patch2d-9voowhJ_.js +816 -0
- package/dist/chunks/patch2d-9voowhJ_.js.br +0 -0
- package/dist/chunks/patch2d-9voowhJ_.js.gz +0 -0
- package/dist/chunks/patch2d-PQxcaE6r.js +886 -0
- package/dist/chunks/patch2d-PQxcaE6r.js.br +0 -0
- package/dist/chunks/patch2d-PQxcaE6r.js.gz +0 -0
- package/dist/graphql.js +12 -1
- package/dist/graphql.js.br +0 -0
- package/dist/graphql.js.gz +0 -0
- package/dist/inAppReportIssueModal/index.js +10 -1
- package/dist/inAppReportIssueModal/index.js.br +0 -0
- package/dist/inAppReportIssueModal/index.js.gz +0 -0
- package/dist/index.js +144 -2
- package/dist/index.js.br +0 -0
- package/dist/index.js.gz +0 -0
- package/dist/recorder.cjs +2 -2
- package/dist/recorder.cjs.br +0 -0
- package/dist/recorder.cjs.gz +0 -0
- package/dist/recorder.js +52 -41
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +4842 -2215
- package/dist/recorder.umd.cjs.br +0 -0
- package/dist/recorder.umd.cjs.gz +0 -0
- package/dist/recording.js +119 -10
- package/dist/recording.js.br +0 -0
- package/dist/recording.js.gz +0 -0
- package/dist/types/canvas/adapters.d.ts +71 -0
- package/dist/types/canvas/canvasManager.d.ts +273 -0
- package/dist/types/canvas/canvasPrivacy.d.ts +184 -0
- package/dist/types/canvas/codec.d.ts +27 -0
- package/dist/types/canvas/deviceClass.d.ts +40 -0
- package/dist/types/canvas/encodeWorker.d.ts +54 -0
- package/dist/types/canvas/governor.d.ts +44 -0
- package/dist/types/canvas/keyframes.d.ts +81 -0
- package/dist/types/canvas/mediaRecorderCapture.d.ts +91 -0
- package/dist/types/canvas/objectLog.d.ts +75 -0
- package/dist/types/canvas/patch2d.d.ts +339 -0
- package/dist/types/canvas/patchGL.d.ts +74 -0
- package/dist/types/canvas/patchWebGPU.d.ts +37 -0
- package/dist/types/canvas/piiDetect.d.ts +60 -0
- package/dist/types/canvas/protocol.d.ts +176 -0
- package/dist/types/canvas/videoCapture.d.ts +67 -0
- package/dist/types/canvas/workerRelay.d.ts +29 -0
- package/dist/types/canvas/workerShim.d.ts +10 -0
- package/dist/types/canvasFrameStore.d.ts +23 -0
- package/dist/types/graphql.d.ts +1 -1
- package/dist/types/index.d.ts +99 -1
- package/dist/types/types.d.ts +40 -0
- package/dist/types/websocket.d.ts +19 -0
- package/dist/websocket.js +210 -0
- package/dist/websocket.js.br +0 -0
- package/dist/websocket.js.gz +0 -0
- package/package.json +7 -1
- package/dist/chunks/chunkSerializer-Cd8y9tID.js.br +0 -0
- package/dist/chunks/chunkSerializer-Cd8y9tID.js.gz +0 -0
- package/dist/chunks/chunkSerializer-pkpgT-FH.js.br +0 -0
- package/dist/chunks/chunkSerializer-pkpgT-FH.js.gz +0 -0
- package/dist/chunks/index-Bvd8oTpV.js.br +0 -0
- package/dist/chunks/index-Bvd8oTpV.js.gz +0 -0
- package/dist/chunks/index-CXvHbLRT.js.br +0 -0
- package/dist/chunks/index-CXvHbLRT.js.gz +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebGPU command capture via object-graph proxying — capture-side port of the
|
|
3
|
+
* validated bench core (test-applications/canvas-recorder-bench/src/capture/
|
|
4
|
+
* patchWebGPU.ts; webgpu_recorder lineage). navigator.gpu and every object
|
|
5
|
+
* reachable from it (adapter, device, queue, encoders, pipelines, textures,
|
|
6
|
+
* views...) are wrapped in recording proxies. Method calls/property hops are
|
|
7
|
+
* logged as meta-ops with interned result ids; descriptors serialize as JSON
|
|
8
|
+
* with {__sfRef} markers for object references; buffer payloads ride the
|
|
9
|
+
* typed-array tag.
|
|
10
|
+
*
|
|
11
|
+
* Replay (player-side, NOT ported here) re-executes the op list against a
|
|
12
|
+
* fresh adapter/device with the recorded canvas context bound to the replay
|
|
13
|
+
* target. Deterministic on the same GPU; pixel keyframes remain the bit-exact
|
|
14
|
+
* truth (hybrid).
|
|
15
|
+
*
|
|
16
|
+
* SDK adaptations: SSR-safe installation, DiscardSink short-circuit (video
|
|
17
|
+
* demotion keeps dirtiness with zero serialize cost), swapGPUSink for the
|
|
18
|
+
* manager, replayGPU dropped (player concern).
|
|
19
|
+
*/
|
|
20
|
+
import { DIRTY_OP, DiscardSink } from "./protocol";
|
|
21
|
+
import { BufferSink, serializeTypedArray, TYPED_ARRAY_KINDS } from "./protocol";
|
|
22
|
+
/** Cap a single serialized buffer payload (writeBuffer/writeTexture data, nested
|
|
23
|
+
* descriptor arrays) so one big upload can't push a frame past the 8 MiB
|
|
24
|
+
* transport cap (canvasManager MAX_FRAME_BYTES) — which would DROP THE WHOLE
|
|
25
|
+
* frame (draws + submit), breaking replay/pick. Mirror patchGL's texture cap:
|
|
26
|
+
* report `buffer-too-large` and degrade that frame to a keyframe. [audit HIGH] */
|
|
27
|
+
const MAX_GPU_RAW_BYTES = 7 * 1024 * 1024;
|
|
28
|
+
const OP_ROOT = "__gpuroot";
|
|
29
|
+
const OP_CTX = "__gpuctx";
|
|
30
|
+
const OP_CALL = "__gpu";
|
|
31
|
+
const OP_PROP = "__gpuprop";
|
|
32
|
+
let installed = false;
|
|
33
|
+
let autoConfig = null;
|
|
34
|
+
/**
|
|
35
|
+
* A10 (WebGPU) — device-loss notifier. WebGPU has no contextlost event; instead
|
|
36
|
+
* each GPUDevice exposes a `lost` promise that resolves when the device is lost
|
|
37
|
+
* (and is never re-created — the app must request a new device). The manager
|
|
38
|
+
* registers a callback here; we resolve it the first time any recorded device's
|
|
39
|
+
* `lost` promise settles, and again (with restored=true) once a NEW device is
|
|
40
|
+
* acquired afterwards. `reason === "destroyed"` (explicit device.destroy()) is
|
|
41
|
+
* NOT a loss to recover from and is ignored.
|
|
42
|
+
*/
|
|
43
|
+
let deviceLostNotify = null;
|
|
44
|
+
let sawDeviceAfterLoss = false;
|
|
45
|
+
let deviceLostFired = false;
|
|
46
|
+
export function setGPUDeviceLostNotifier(fn) {
|
|
47
|
+
deviceLostNotify = fn;
|
|
48
|
+
sawDeviceAfterLoss = false;
|
|
49
|
+
deviceLostFired = false;
|
|
50
|
+
}
|
|
51
|
+
function watchDeviceLost(device) {
|
|
52
|
+
const lost = device.lost;
|
|
53
|
+
if (!lost || typeof lost.then !== "function")
|
|
54
|
+
return;
|
|
55
|
+
// A NEW device after a prior loss = the app recovered.
|
|
56
|
+
if (deviceLostFired && !sawDeviceAfterLoss) {
|
|
57
|
+
sawDeviceAfterLoss = true;
|
|
58
|
+
try {
|
|
59
|
+
deviceLostNotify?.(false);
|
|
60
|
+
}
|
|
61
|
+
catch { /* never break host app */ }
|
|
62
|
+
}
|
|
63
|
+
lost.then((info) => {
|
|
64
|
+
// Explicit destroy() is intentional teardown, not a recoverable loss.
|
|
65
|
+
if (info && info.reason === "destroyed")
|
|
66
|
+
return;
|
|
67
|
+
if (deviceLostFired)
|
|
68
|
+
return;
|
|
69
|
+
deviceLostFired = true;
|
|
70
|
+
try {
|
|
71
|
+
deviceLostNotify?.(true);
|
|
72
|
+
}
|
|
73
|
+
catch { /* never break host app */ }
|
|
74
|
+
}).catch(() => { });
|
|
75
|
+
}
|
|
76
|
+
/** single global stream — WebGPU's object graph is page-scoped, not per-canvas */
|
|
77
|
+
let sink = null;
|
|
78
|
+
let nowFn = () => 0;
|
|
79
|
+
let cpu = 0;
|
|
80
|
+
// 3M base keeps WebGPU object ids clear of patchGL's object (1M) and asset (2M)
|
|
81
|
+
// ranges, so the two recorders' ids never collide if a stream is ever merged. [audit LOW]
|
|
82
|
+
let nextId = 3_000_000;
|
|
83
|
+
let rootEmitted = false;
|
|
84
|
+
let recordedCanvas = null;
|
|
85
|
+
let attachedCanvas = null;
|
|
86
|
+
const idOf = new WeakMap();
|
|
87
|
+
const proxyOf = new WeakMap();
|
|
88
|
+
const targetOf = new WeakMap();
|
|
89
|
+
function emit(op) {
|
|
90
|
+
sink?.onOp(op);
|
|
91
|
+
}
|
|
92
|
+
function intern(obj) {
|
|
93
|
+
let id = idOf.get(obj);
|
|
94
|
+
if (id === undefined) {
|
|
95
|
+
id = nextId++;
|
|
96
|
+
idOf.set(obj, id);
|
|
97
|
+
}
|
|
98
|
+
return id;
|
|
99
|
+
}
|
|
100
|
+
function deproxy(v) {
|
|
101
|
+
if (v && typeof v === "object" && targetOf.has(v))
|
|
102
|
+
return targetOf.get(v);
|
|
103
|
+
return v;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* WebIDL brand checks reject Proxy-wrapped platform objects, including ones
|
|
107
|
+
* NESTED inside descriptors ({device}, {layout}, render-pass views...) —
|
|
108
|
+
* native calls need the real objects all the way down.
|
|
109
|
+
*/
|
|
110
|
+
function deepDeproxy(v) {
|
|
111
|
+
if (!v || typeof v !== "object")
|
|
112
|
+
return v;
|
|
113
|
+
if (targetOf.has(v))
|
|
114
|
+
return targetOf.get(v);
|
|
115
|
+
if (ArrayBuffer.isView(v) || v instanceof ArrayBuffer)
|
|
116
|
+
return v;
|
|
117
|
+
if (Array.isArray(v)) {
|
|
118
|
+
let changed = false;
|
|
119
|
+
const out = v.map((x) => {
|
|
120
|
+
const d = deepDeproxy(x);
|
|
121
|
+
if (d !== x)
|
|
122
|
+
changed = true;
|
|
123
|
+
return d;
|
|
124
|
+
});
|
|
125
|
+
return changed ? out : v;
|
|
126
|
+
}
|
|
127
|
+
const proto = Object.getPrototypeOf(v);
|
|
128
|
+
if (proto === Object.prototype || proto === null) {
|
|
129
|
+
let changed = false;
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const [k, val] of Object.entries(v)) {
|
|
132
|
+
const d = deepDeproxy(val);
|
|
133
|
+
if (d !== val)
|
|
134
|
+
changed = true;
|
|
135
|
+
out[k] = d;
|
|
136
|
+
}
|
|
137
|
+
return changed ? out : v;
|
|
138
|
+
}
|
|
139
|
+
return v;
|
|
140
|
+
}
|
|
141
|
+
// SerializedArg has no dedicated descriptor variant; descriptors are carried
|
|
142
|
+
// as a string arg prefixed so replay can tell them from plain strings.
|
|
143
|
+
const JSON_PREFIX = "json:";
|
|
144
|
+
function serializeGPUArg(a) {
|
|
145
|
+
if (a === null || a === undefined)
|
|
146
|
+
return null;
|
|
147
|
+
switch (typeof a) {
|
|
148
|
+
case "number": return Number.isFinite(a) ? a : 0;
|
|
149
|
+
case "string": return a;
|
|
150
|
+
case "boolean": return a;
|
|
151
|
+
case "object": break;
|
|
152
|
+
default: return { $: "unsupported", kind: typeof a };
|
|
153
|
+
}
|
|
154
|
+
const raw = deproxy(a);
|
|
155
|
+
if (ArrayBuffer.isView(raw) || raw instanceof ArrayBuffer) {
|
|
156
|
+
if (raw.byteLength > MAX_GPU_RAW_BYTES) {
|
|
157
|
+
return { $: "unsupported", kind: "buffer-too-large" };
|
|
158
|
+
}
|
|
159
|
+
return serializeTypedArray(raw);
|
|
160
|
+
}
|
|
161
|
+
const ref = idOf.get(raw);
|
|
162
|
+
if (ref !== undefined)
|
|
163
|
+
return { $: "obj", id: ref };
|
|
164
|
+
// descriptor object/array: JSON with embedded object refs
|
|
165
|
+
try {
|
|
166
|
+
const json = JSON.stringify(raw, (_k, v) => {
|
|
167
|
+
if (v && typeof v === "object") {
|
|
168
|
+
const t = deproxy(v);
|
|
169
|
+
const r = idOf.get(t);
|
|
170
|
+
if (r !== undefined)
|
|
171
|
+
return { __sfRef: r };
|
|
172
|
+
if (ArrayBuffer.isView(t)) {
|
|
173
|
+
const view = t;
|
|
174
|
+
if (view.byteLength > MAX_GPU_RAW_BYTES)
|
|
175
|
+
return { __sfUnsupported: "buffer-too-large" };
|
|
176
|
+
// Tag the element kind so the player reconstructs the EXACT typed-array
|
|
177
|
+
// type (else an integer array is corrupted to Float32). [audit MEDIUM]
|
|
178
|
+
const k = TYPED_ARRAY_KINDS.findIndex((c) => view instanceof c);
|
|
179
|
+
if (k < 0)
|
|
180
|
+
return { __sfUnsupported: "typed-array-kind" }; // BigInt64Array etc.
|
|
181
|
+
return { __sfTa: Array.from(view), __sfK: k };
|
|
182
|
+
}
|
|
183
|
+
return t === v ? v : t;
|
|
184
|
+
}
|
|
185
|
+
return v;
|
|
186
|
+
});
|
|
187
|
+
return JSON_PREFIX + json;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return { $: "unsupported", kind: raw.constructor?.name ?? "object" };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const serializeArgWire = serializeGPUArg;
|
|
194
|
+
function shouldProxyResult(v) {
|
|
195
|
+
if (!v || typeof v !== "object")
|
|
196
|
+
return false;
|
|
197
|
+
const name = v.constructor?.name ?? "";
|
|
198
|
+
return name.startsWith("GPU");
|
|
199
|
+
}
|
|
200
|
+
function wrap(obj) {
|
|
201
|
+
const existing = proxyOf.get(obj);
|
|
202
|
+
if (existing)
|
|
203
|
+
return existing;
|
|
204
|
+
// A10: arm device-loss watching the first time we wrap a GPUDevice.
|
|
205
|
+
if (obj.constructor?.name === "GPUDevice") {
|
|
206
|
+
watchDeviceLost(obj);
|
|
207
|
+
}
|
|
208
|
+
const proxy = new Proxy(obj, {
|
|
209
|
+
get(target, prop, _receiver) {
|
|
210
|
+
const v = Reflect.get(target, prop, target);
|
|
211
|
+
if (typeof prop === "symbol")
|
|
212
|
+
return v;
|
|
213
|
+
if (typeof v === "function") {
|
|
214
|
+
return (...args) => {
|
|
215
|
+
const t0 = performance.now();
|
|
216
|
+
const nativeArgs = args.map(deepDeproxy);
|
|
217
|
+
const result = v.apply(target, nativeArgs);
|
|
218
|
+
if (sink instanceof DiscardSink) {
|
|
219
|
+
// Video-mode dirtiness only: keep wrapping for graph consistency,
|
|
220
|
+
// skip serialization + emission entirely.
|
|
221
|
+
sink.onOp(DIRTY_OP);
|
|
222
|
+
if (result && typeof result === "object" && typeof result.then === "function") {
|
|
223
|
+
return result.then((val) => val && typeof val === "object" && shouldProxyResult(val) ? (intern(val), wrap(val)) : val);
|
|
224
|
+
}
|
|
225
|
+
return shouldProxyResult(result) ? (intern(result), wrap(result)) : result;
|
|
226
|
+
}
|
|
227
|
+
if (sink) {
|
|
228
|
+
const tId = intern(target);
|
|
229
|
+
if (result && typeof result === "object" && typeof result.then === "function") {
|
|
230
|
+
const rid = nextId++;
|
|
231
|
+
emit({
|
|
232
|
+
t: nowFn(), op: OP_CALL, objcall: true,
|
|
233
|
+
args: [tId, String(prop), rid, ...args.map(serializeArgWire)],
|
|
234
|
+
});
|
|
235
|
+
cpu += performance.now() - t0;
|
|
236
|
+
return result.then((val) => {
|
|
237
|
+
if (val && typeof val === "object") {
|
|
238
|
+
idOf.set(val, rid);
|
|
239
|
+
return wrap(val);
|
|
240
|
+
}
|
|
241
|
+
return val;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
let rid = 0;
|
|
245
|
+
if (shouldProxyResult(result))
|
|
246
|
+
rid = intern(result);
|
|
247
|
+
emit({
|
|
248
|
+
t: nowFn(), op: OP_CALL, objcall: true,
|
|
249
|
+
args: [tId, String(prop), rid, ...args.map(serializeArgWire)],
|
|
250
|
+
});
|
|
251
|
+
cpu += performance.now() - t0;
|
|
252
|
+
if (shouldProxyResult(result))
|
|
253
|
+
return wrap(result);
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
if (result && typeof result === "object" && typeof result.then === "function") {
|
|
257
|
+
return result.then((val) => val && typeof val === "object" && shouldProxyResult(val) ? (intern(val), wrap(val)) : val);
|
|
258
|
+
}
|
|
259
|
+
return shouldProxyResult(result) ? (intern(result), wrap(result)) : result;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (shouldProxyResult(v)) {
|
|
263
|
+
const known = idOf.has(v);
|
|
264
|
+
const rid = intern(v);
|
|
265
|
+
if (!known && sink && !(sink instanceof DiscardSink)) {
|
|
266
|
+
emit({ t: nowFn(), op: OP_PROP, objcall: true, args: [intern(target), String(prop), rid] });
|
|
267
|
+
}
|
|
268
|
+
return wrap(v);
|
|
269
|
+
}
|
|
270
|
+
return v;
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
proxyOf.set(obj, proxy);
|
|
274
|
+
targetOf.set(proxy, obj);
|
|
275
|
+
return proxy;
|
|
276
|
+
}
|
|
277
|
+
export function setGPUAutoAttach(config) {
|
|
278
|
+
autoConfig = config;
|
|
279
|
+
if (config) {
|
|
280
|
+
nowFn = config.now;
|
|
281
|
+
if (!sink) {
|
|
282
|
+
sink = new BufferSink();
|
|
283
|
+
rootEmitted = false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export function pruneUnboundGPURecorders() {
|
|
288
|
+
if (sink instanceof BufferSink && !attachedCanvas) {
|
|
289
|
+
if (!recordedCanvas)
|
|
290
|
+
sink = null; // nothing GPU happened this run
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** The canvas (if any) that created a webgpu context while armed. */
|
|
294
|
+
export function getGPURecordedCanvas() {
|
|
295
|
+
return recordedCanvas;
|
|
296
|
+
}
|
|
297
|
+
export function installGPUHooks() {
|
|
298
|
+
if (installed)
|
|
299
|
+
return;
|
|
300
|
+
installed = true;
|
|
301
|
+
if (typeof navigator === "undefined" || !("gpu" in navigator))
|
|
302
|
+
return;
|
|
303
|
+
const realGpu = navigator.gpu;
|
|
304
|
+
Object.defineProperty(navigator, "gpu", {
|
|
305
|
+
configurable: true,
|
|
306
|
+
get() {
|
|
307
|
+
if (autoConfig || sink) {
|
|
308
|
+
if (!rootEmitted) {
|
|
309
|
+
rootEmitted = true;
|
|
310
|
+
const rid = intern(realGpu);
|
|
311
|
+
emit({ t: nowFn(), op: OP_ROOT, args: [rid] });
|
|
312
|
+
}
|
|
313
|
+
return wrap(realGpu);
|
|
314
|
+
}
|
|
315
|
+
return realGpu;
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
// canvas.getContext('webgpu') -> proxied context (chained over prior patches)
|
|
319
|
+
if (typeof HTMLCanvasElement === "undefined")
|
|
320
|
+
return;
|
|
321
|
+
const origGetContext = HTMLCanvasElement.prototype.getContext;
|
|
322
|
+
HTMLCanvasElement.prototype.getContext = function (type, attrs) {
|
|
323
|
+
const result = origGetContext.call(this, type, attrs);
|
|
324
|
+
if (type === "webgpu" && result && (autoConfig || sink)) {
|
|
325
|
+
recordedCanvas = this;
|
|
326
|
+
const known = idOf.has(result);
|
|
327
|
+
const rid = intern(result);
|
|
328
|
+
if (!known)
|
|
329
|
+
emit({ t: nowFn(), op: OP_CTX, args: [rid] });
|
|
330
|
+
return wrap(result);
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
export function attachGPURecorder(canvas, strategySink, now) {
|
|
336
|
+
if (recordedCanvas !== canvas || !sink)
|
|
337
|
+
return null;
|
|
338
|
+
attachedCanvas = canvas;
|
|
339
|
+
nowFn = now;
|
|
340
|
+
const buffered = sink;
|
|
341
|
+
sink = strategySink;
|
|
342
|
+
if (buffered instanceof BufferSink) {
|
|
343
|
+
for (const op of buffered.ops)
|
|
344
|
+
strategySink.onOp(op);
|
|
345
|
+
for (const n of buffered.notes)
|
|
346
|
+
strategySink.note(n);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
detach() {
|
|
350
|
+
sink = null;
|
|
351
|
+
attachedCanvas = null;
|
|
352
|
+
recordedCanvas = null;
|
|
353
|
+
},
|
|
354
|
+
cpuMs: () => cpu,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/** Swap the page-global GPU sink (manager demotion path). */
|
|
358
|
+
export function swapGPUSink(s) {
|
|
359
|
+
if (sink)
|
|
360
|
+
sink = s;
|
|
361
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A13 — content-level PII detection + redaction shared between the rrweb input
|
|
3
|
+
* masker (`maskInputFn` in recording.tsx) and the canvas text path
|
|
4
|
+
* (`canvasPrivacy.ts`). Extracted so both call sites use ONE implementation of
|
|
5
|
+
* the credit-card / SSN / DOB detectors and their partial-mask formats.
|
|
6
|
+
*
|
|
7
|
+
* The "record*" flags are INVERTED privacy intent: `recordCreditCardInfo:false`
|
|
8
|
+
* means "do NOT record credit-card content" → redact it. A flag that is TRUE
|
|
9
|
+
* (or undefined → treated as record-everything for that type) leaves the
|
|
10
|
+
* matching content untouched. This mirrors the historical maskInputFn behavior,
|
|
11
|
+
* which always partial-masked detected CC/SSN content regardless of flags; here
|
|
12
|
+
* the canvas path gates on the flags so an account that explicitly opts INTO
|
|
13
|
+
* recording a PII type is honored.
|
|
14
|
+
*
|
|
15
|
+
* SSR rule: pure string functions, no window/document/navigator access. Safe to
|
|
16
|
+
* import at module scope anywhere.
|
|
17
|
+
*/
|
|
18
|
+
// ─── Detection patterns ──────────────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* 13–16 digit run, optionally separated by spaces/hyphens (matching the
|
|
21
|
+
* historical `maskInputFn` pattern). This is a CANDIDATE matcher only — a bare
|
|
22
|
+
* 16-digit number (a phone number, order id, SKU…) also matches, so callers
|
|
23
|
+
* combine it with `passesLuhn` to cut false positives.
|
|
24
|
+
*/
|
|
25
|
+
/** Cheap pre-check: PII detection can be skipped entirely for digit-free text. */
|
|
26
|
+
const HAS_DIGIT = /\d/;
|
|
27
|
+
const CREDIT_CARD_CANDIDATE = /\b(?:\d[ -]*?){13,16}\b/;
|
|
28
|
+
const SSN_PATTERN = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
29
|
+
/**
|
|
30
|
+
* Date-of-birth: 1–2 digit day/month, a `/`, `-`, or `.` separator, and a 2–4
|
|
31
|
+
* digit year. Deliberately permissive (m/d/y and d/m/y both match) — this is a
|
|
32
|
+
* privacy backstop, not a calendar validator. Does NOT match an SSN (which uses
|
|
33
|
+
* the 3-2-4 grouping with hyphens only and is caught first by the SSN rule).
|
|
34
|
+
*/
|
|
35
|
+
const DOB_PATTERN = /\b\d{1,2}[/\-.]\d{1,2}[/\-.]\d{2,4}\b/;
|
|
36
|
+
/** Global variants for multi-match redaction within a single string. */
|
|
37
|
+
const CREDIT_CARD_CANDIDATE_G = /\b(?:\d[ -]*?){13,16}\b/g;
|
|
38
|
+
const SSN_PATTERN_G = /\b\d{3}-\d{2}-\d{4}\b/g;
|
|
39
|
+
const DOB_PATTERN_G = /\b\d{1,2}[/\-.]\d{1,2}[/\-.]\d{2,4}\b/g;
|
|
40
|
+
/**
|
|
41
|
+
* Luhn checksum over the digits of a candidate credit-card string. Accepts the
|
|
42
|
+
* raw matched substring (may contain spaces/hyphens) OR a pre-stripped digit
|
|
43
|
+
* string. Returns false for anything without 13–16 digits so non-card numbers
|
|
44
|
+
* (phones, SKUs) that happen to match the candidate regex are rejected.
|
|
45
|
+
*/
|
|
46
|
+
export function passesLuhn(digits) {
|
|
47
|
+
// Strip any non-digit separators the candidate matcher allowed through.
|
|
48
|
+
let only = "";
|
|
49
|
+
for (let i = 0; i < digits.length; i++) {
|
|
50
|
+
const c = digits.charCodeAt(i);
|
|
51
|
+
if (c >= 48 && c <= 57)
|
|
52
|
+
only += digits[i];
|
|
53
|
+
}
|
|
54
|
+
const len = only.length;
|
|
55
|
+
if (len < 13 || len > 16)
|
|
56
|
+
return false;
|
|
57
|
+
let sum = 0;
|
|
58
|
+
let dbl = false;
|
|
59
|
+
// Walk right-to-left, doubling every second digit (Luhn).
|
|
60
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
61
|
+
let d = only.charCodeAt(i) - 48;
|
|
62
|
+
if (dbl) {
|
|
63
|
+
d *= 2;
|
|
64
|
+
if (d > 9)
|
|
65
|
+
d -= 9;
|
|
66
|
+
}
|
|
67
|
+
sum += d;
|
|
68
|
+
dbl = !dbl;
|
|
69
|
+
}
|
|
70
|
+
return sum % 10 === 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* True when `s` contains a 13–16 digit run that ALSO passes the Luhn check.
|
|
74
|
+
* The Luhn gate removes the vast majority of false positives (phone numbers,
|
|
75
|
+
* order ids) the bare digit-run regex would otherwise flag.
|
|
76
|
+
*/
|
|
77
|
+
export function looksLikeCreditCard(s) {
|
|
78
|
+
if (typeof s !== "string")
|
|
79
|
+
return false;
|
|
80
|
+
CREDIT_CARD_CANDIDATE_G.lastIndex = 0;
|
|
81
|
+
let m;
|
|
82
|
+
while ((m = CREDIT_CARD_CANDIDATE_G.exec(s)) !== null) {
|
|
83
|
+
if (passesLuhn(m[0]))
|
|
84
|
+
return true;
|
|
85
|
+
// Avoid an infinite loop on a zero-width match (cannot happen for this
|
|
86
|
+
// pattern, but defensive).
|
|
87
|
+
if (m.index === CREDIT_CARD_CANDIDATE_G.lastIndex)
|
|
88
|
+
CREDIT_CARD_CANDIDATE_G.lastIndex++;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
/** True when `s` contains a `NNN-NN-NNNN` SSN-formatted run. */
|
|
93
|
+
export function looksLikeSsn(s) {
|
|
94
|
+
if (typeof s !== "string")
|
|
95
|
+
return false;
|
|
96
|
+
return SSN_PATTERN.test(s);
|
|
97
|
+
}
|
|
98
|
+
/** True when `s` contains a `D/M/Y`-style date (any of `/ - .` separators). */
|
|
99
|
+
export function looksLikeDob(s) {
|
|
100
|
+
if (typeof s !== "string")
|
|
101
|
+
return false;
|
|
102
|
+
return DOB_PATTERN.test(s);
|
|
103
|
+
}
|
|
104
|
+
// ─── Partial-mask formats (player-readable, length-stable-ish) ───────────────
|
|
105
|
+
/** "**** **** **** "+last4 — the historical maskInputFn credit-card format. */
|
|
106
|
+
export function redactCreditCard(s) {
|
|
107
|
+
return "**** **** **** " + s.slice(-4);
|
|
108
|
+
}
|
|
109
|
+
/** "***-**-"+last4 — the historical maskInputFn SSN format. */
|
|
110
|
+
export function redactSsn(s) {
|
|
111
|
+
return "***-**-" + s.slice(-4);
|
|
112
|
+
}
|
|
113
|
+
/** DOB partial mask: two masked groups + "/" + last 4 chars. */
|
|
114
|
+
export function redactDob(s) {
|
|
115
|
+
return "**/**/" + s.slice(-4);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Apply the partial-mask redactions to every PII match in `text`, but ONLY for
|
|
119
|
+
* the flags that are explicitly FALSE (privacy intent: "do not record this PII
|
|
120
|
+
* type"). A flag that is `true` or `undefined` leaves that PII type intact.
|
|
121
|
+
* Returns the original string when nothing matched or every relevant flag is on.
|
|
122
|
+
*
|
|
123
|
+
* Each enabled redactor replaces ALL of its matches in the string (a line like
|
|
124
|
+
* "DOB 01/02/1990 card 4111 1111 1111 1111" redacts both). Order:
|
|
125
|
+
* credit-card → SSN → DOB so a CC's internal digits are masked before the DOB
|
|
126
|
+
* pass scans (the partial-mask output contains no date-shaped runs).
|
|
127
|
+
*/
|
|
128
|
+
export function redactSensitiveContent(text, flags) {
|
|
129
|
+
if (typeof text !== "string" || text.length === 0)
|
|
130
|
+
return text;
|
|
131
|
+
// Hot-path fast-out: every PII pattern (CC/SSN/DOB) requires digits, so a
|
|
132
|
+
// string with no digit can never match. This keeps the THREE regex passes off
|
|
133
|
+
// the common case — chart labels, axis titles, button captions — which the 2D
|
|
134
|
+
// text path hits on every fillText/strokeText (respects the A34 hot-path work).
|
|
135
|
+
if (!HAS_DIGIT.test(text))
|
|
136
|
+
return text;
|
|
137
|
+
let out = text;
|
|
138
|
+
// Credit cards: replace each Luhn-valid candidate run.
|
|
139
|
+
if (flags.recordCreditCardInfo === false) {
|
|
140
|
+
CREDIT_CARD_CANDIDATE_G.lastIndex = 0;
|
|
141
|
+
out = out.replace(CREDIT_CARD_CANDIDATE_G, (match) => passesLuhn(match) ? redactCreditCard(match) : match);
|
|
142
|
+
}
|
|
143
|
+
// SSNs.
|
|
144
|
+
if (flags.recordSsn === false) {
|
|
145
|
+
SSN_PATTERN_G.lastIndex = 0;
|
|
146
|
+
out = out.replace(SSN_PATTERN_G, (match) => redactSsn(match));
|
|
147
|
+
}
|
|
148
|
+
// DOBs. Run last so the asterisk output of the CC/SSN passes (which contains
|
|
149
|
+
// no date-shaped digit runs) is never re-scanned into a DOB false positive.
|
|
150
|
+
if (flags.recordDob === false) {
|
|
151
|
+
DOB_PATTERN_G.lastIndex = 0;
|
|
152
|
+
out = out.replace(DOB_PATTERN_G, (match) => redactDob(match));
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared canvas-recording protocol types + the SFCV binary frame format.
|
|
3
|
+
*
|
|
4
|
+
* Types are ported from the validated bench core
|
|
5
|
+
* (test-applications/canvas-recorder-bench/src/capture/patch2d.ts); the SFCV
|
|
6
|
+
* frame builder/parser implements the canvas-recording-pipeline contract §1:
|
|
7
|
+
*
|
|
8
|
+
* bytes 0..3 magic "SFC1" (0x53 0x46 0x43 0x31)
|
|
9
|
+
* bytes 4..7 u32 LE: headerLen
|
|
10
|
+
* bytes 8.. header JSON (utf8, headerLen bytes)
|
|
11
|
+
* rest payload bytes
|
|
12
|
+
*
|
|
13
|
+
* SSR rule: nothing in this module touches window/document/navigator at
|
|
14
|
+
* module scope.
|
|
15
|
+
*/
|
|
16
|
+
export const TYPED_ARRAY_KINDS = [
|
|
17
|
+
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
|
|
18
|
+
Int32Array, Uint32Array, Float32Array, Float64Array,
|
|
19
|
+
];
|
|
20
|
+
/** Copy a typed array / ArrayBuffer into a serialized arg AT CALL TIME. */
|
|
21
|
+
export function serializeTypedArray(v) {
|
|
22
|
+
if (v instanceof ArrayBuffer) {
|
|
23
|
+
return { $: "ta", k: 1, b: new Uint8Array(v.slice(0)) };
|
|
24
|
+
}
|
|
25
|
+
const k = TYPED_ARRAY_KINDS.findIndex((c) => v instanceof c);
|
|
26
|
+
if (k < 0)
|
|
27
|
+
return { $: "unsupported", kind: v.constructor?.name ?? "view" };
|
|
28
|
+
const bytes = new Uint8Array(v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength));
|
|
29
|
+
return { $: "ta", k, b: bytes };
|
|
30
|
+
}
|
|
31
|
+
export function deserializeTypedArray(a) {
|
|
32
|
+
const Ctor = TYPED_ARRAY_KINDS[a.k] ?? Uint8Array;
|
|
33
|
+
const buf = a.b.buffer.slice(a.b.byteOffset, a.b.byteOffset + a.b.byteLength);
|
|
34
|
+
return new Ctor(buf);
|
|
35
|
+
}
|
|
36
|
+
export class BufferSink {
|
|
37
|
+
ops = [];
|
|
38
|
+
assetEvents = [];
|
|
39
|
+
assetData = [];
|
|
40
|
+
notes = [];
|
|
41
|
+
onOp(op) { this.ops.push(op); }
|
|
42
|
+
onAssetBytes(id, bytes, t) { this.assetEvents.push({ id, bytes, t }); }
|
|
43
|
+
onAssetData(info, payload) { this.assetData.push({ info, payload }); }
|
|
44
|
+
note(m) { if (!this.notes.includes(m))
|
|
45
|
+
this.notes.push(m); }
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Video-mode dirtiness sink: keeps the patch recorder attached purely to
|
|
49
|
+
* drive the per-canvas dirty flag — every op is discarded with ZERO
|
|
50
|
+
* serialize/encode cost (recorders short-circuit on `instanceof DiscardSink`
|
|
51
|
+
* before serializing arguments).
|
|
52
|
+
*/
|
|
53
|
+
export class DiscardSink {
|
|
54
|
+
onDirty;
|
|
55
|
+
constructor(onDirty) {
|
|
56
|
+
this.onDirty = onDirty;
|
|
57
|
+
}
|
|
58
|
+
onOp(_op) { this.onDirty(); }
|
|
59
|
+
onAssetBytes(_id, _bytes, _t) { }
|
|
60
|
+
note(_m) { }
|
|
61
|
+
}
|
|
62
|
+
/** Shared zero-cost op handed to DiscardSink (its contents are never read). */
|
|
63
|
+
export const DIRTY_OP = { t: 0, op: "", args: [] };
|
|
64
|
+
const utf8e = new TextEncoder();
|
|
65
|
+
const utf8d = new TextDecoder();
|
|
66
|
+
const MAGIC = [0x53, 0x46, 0x43, 0x31]; // "SFC1"
|
|
67
|
+
export function buildFrame(header, payload) {
|
|
68
|
+
const payloadBytes = payload == null
|
|
69
|
+
? new Uint8Array(0)
|
|
70
|
+
: payload instanceof ArrayBuffer
|
|
71
|
+
? new Uint8Array(payload)
|
|
72
|
+
: payload;
|
|
73
|
+
const headerBytes = utf8e.encode(JSON.stringify({ ...header, pLen: payloadBytes.byteLength }));
|
|
74
|
+
const buf = new ArrayBuffer(8 + headerBytes.length + payloadBytes.byteLength);
|
|
75
|
+
const u8 = new Uint8Array(buf);
|
|
76
|
+
u8[0] = MAGIC[0];
|
|
77
|
+
u8[1] = MAGIC[1];
|
|
78
|
+
u8[2] = MAGIC[2];
|
|
79
|
+
u8[3] = MAGIC[3];
|
|
80
|
+
new DataView(buf).setUint32(4, headerBytes.length, true);
|
|
81
|
+
u8.set(headerBytes, 8);
|
|
82
|
+
u8.set(payloadBytes, 8 + headerBytes.length);
|
|
83
|
+
return buf;
|
|
84
|
+
}
|
|
85
|
+
/** Parse one frame or a concatenation of frames. Throws on corrupt input. */
|
|
86
|
+
export function parseFrames(bytes) {
|
|
87
|
+
const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
88
|
+
const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
|
|
89
|
+
const frames = [];
|
|
90
|
+
let pos = 0;
|
|
91
|
+
while (pos < u8.length) {
|
|
92
|
+
if (pos + 8 > u8.length)
|
|
93
|
+
throw new Error(`SFCV: truncated frame at ${pos}`);
|
|
94
|
+
if (u8[pos] !== MAGIC[0] || u8[pos + 1] !== MAGIC[1] || u8[pos + 2] !== MAGIC[2] || u8[pos + 3] !== MAGIC[3]) {
|
|
95
|
+
throw new Error(`SFCV: bad magic at ${pos}`);
|
|
96
|
+
}
|
|
97
|
+
const headerLen = view.getUint32(pos + 4, true);
|
|
98
|
+
const headerEnd = pos + 8 + headerLen;
|
|
99
|
+
if (headerEnd > u8.length)
|
|
100
|
+
throw new Error(`SFCV: truncated header at ${pos}`);
|
|
101
|
+
const header = JSON.parse(utf8d.decode(u8.subarray(pos + 8, headerEnd)));
|
|
102
|
+
if (header.v !== 1)
|
|
103
|
+
throw new Error(`SFCV: unsupported version ${header.v}`);
|
|
104
|
+
const pLen = typeof header.pLen === "number" ? header.pLen : u8.length - headerEnd;
|
|
105
|
+
const payloadEnd = headerEnd + pLen;
|
|
106
|
+
if (payloadEnd > u8.length)
|
|
107
|
+
throw new Error(`SFCV: truncated payload at ${pos}`);
|
|
108
|
+
frames.push({ header, payload: u8.slice(headerEnd, payloadEnd) });
|
|
109
|
+
pos = payloadEnd;
|
|
110
|
+
}
|
|
111
|
+
return frames;
|
|
112
|
+
}
|
|
113
|
+
/** Chunked base64 for ArrayBuffers (video-init decoder descriptions). */
|
|
114
|
+
export function bytesToBase64(bytes) {
|
|
115
|
+
const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
116
|
+
let s = "";
|
|
117
|
+
const CHUNK = 0x8000;
|
|
118
|
+
for (let i = 0; i < u8.length; i += CHUNK) {
|
|
119
|
+
s += String.fromCharCode.apply(null, u8.subarray(i, i + CHUNK));
|
|
120
|
+
}
|
|
121
|
+
return btoa(s);
|
|
122
|
+
}
|
|
Binary file
|
|
Binary file
|