@sailfish-ai/recorder 1.12.4 → 1.12.6
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-BYn3_oyo.js +1440 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js.br +0 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js.gz +0 -0
- package/dist/chunks/canvasManager-CoX5072G.js +1456 -0
- package/dist/chunks/canvasManager-CoX5072G.js.br +0 -0
- package/dist/chunks/canvasManager-CoX5072G.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-FQtY90Av.js → chunkSerializer-C_K0Wyk0.js} +1 -1
- package/dist/chunks/chunkSerializer-C_K0Wyk0.js.br +0 -0
- package/dist/chunks/chunkSerializer-C_K0Wyk0.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-DDukZpgl.js → chunkSerializer-CghPitF4.js} +1 -1
- package/dist/chunks/chunkSerializer-CghPitF4.js.br +0 -0
- package/dist/chunks/chunkSerializer-CghPitF4.js.gz +0 -0
- package/dist/chunks/{index-C-qbsfKe.js → index-BIj4l3CV.js} +1028 -650
- package/dist/chunks/index-BIj4l3CV.js.br +0 -0
- package/dist/chunks/index-BIj4l3CV.js.gz +0 -0
- package/dist/chunks/{index-D6axlCRu.js → index-DIYomvdD.js} +1119 -720
- package/dist/chunks/index-DIYomvdD.js.br +0 -0
- package/dist/chunks/index-DIYomvdD.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 +48 -0
- 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 +4847 -2212
- 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-DDukZpgl.js.br +0 -0
- package/dist/chunks/chunkSerializer-DDukZpgl.js.gz +0 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js.br +0 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js.gz +0 -0
- package/dist/chunks/index-C-qbsfKe.js.br +0 -0
- package/dist/chunks/index-C-qbsfKe.js.gz +0 -0
- package/dist/chunks/index-D6axlCRu.js.br +0 -0
- package/dist/chunks/index-D6axlCRu.js.gz +0 -0
package/dist/recorder.umd.cjs.br
CHANGED
|
Binary file
|
package/dist/recorder.umd.cjs.gz
CHANGED
|
Binary file
|
package/dist/recording.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getCallerLocation, getCallerLocationFromTrace, } from "./sourceLocation
|
|
|
5
5
|
import suppressConsoleLogsDuringCall from "./suppressConsoleLogsDuringCall";
|
|
6
6
|
import { yieldToMain } from "./scheduler";
|
|
7
7
|
import { buildMaskInputFn, makeMaskTextFn } from "./privacyMask";
|
|
8
|
+
import { looksLikeCreditCard, looksLikeSsn, redactCreditCard, redactSsn, redactDob, } from "./canvas/piiDetect";
|
|
8
9
|
// rrweb's full set of input-type / tag keys — used to expand
|
|
9
10
|
// maskInputOptions to all-on when admin set a maskInputSelector, so rrweb
|
|
10
11
|
// routes every input through our wrapper which then enforces precise
|
|
@@ -27,7 +28,7 @@ const RRWEB_ALL_INPUT_TYPES = {
|
|
|
27
28
|
select: true,
|
|
28
29
|
password: true,
|
|
29
30
|
};
|
|
30
|
-
import { getCachedHrefNoQuery, initializeWebSocket, sendEvent } from "./websocket";
|
|
31
|
+
import { getCachedHrefNoQuery, initializeWebSocket, isTransportSaturated, sendEvent, wsSendBinary, } from "./websocket";
|
|
31
32
|
// Module-level reference to rrweb record, populated after dynamic import
|
|
32
33
|
let _record = null;
|
|
33
34
|
const MASK_CLASS = "sailfishSanitize";
|
|
@@ -92,20 +93,25 @@ export function maskInputFn(text, node) {
|
|
|
92
93
|
// leaks user input to the WS payload. Specific PII formats keep their
|
|
93
94
|
// length-preserving partial masks for player readability; everything else
|
|
94
95
|
// falls back to a full-asterisk mask (matching rrweb's built-in default).
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
//
|
|
97
|
+
// A13: the credit-card / SSN / DOB detectors and partial-mask formats now
|
|
98
|
+
// live in canvas/piiDetect so the canvas text path and this input masker
|
|
99
|
+
// share one implementation. Behavior here is unchanged: rrweb already chose
|
|
100
|
+
// to mask, so we always emit a masked value (the record* flags are NOT
|
|
101
|
+
// consulted here — they gate the canvas content-detection path, not rrweb's
|
|
102
|
+
// own masking decision). `looksLikeCreditCard` additionally Luhn-checks the
|
|
103
|
+
// candidate run, so a 16-digit non-card input now falls through to the
|
|
104
|
+
// explicit data-cc / autocomplete checks or the full-asterisk fallback.
|
|
99
105
|
if (node.hasAttribute("data-cc") ||
|
|
100
106
|
(node.getAttribute("autocomplete")?.startsWith("cc-") ?? false) ||
|
|
101
|
-
|
|
102
|
-
return
|
|
107
|
+
looksLikeCreditCard(text)) {
|
|
108
|
+
return redactCreditCard(text);
|
|
103
109
|
}
|
|
104
|
-
else if (node.hasAttribute("data-ssn") ||
|
|
105
|
-
return
|
|
110
|
+
else if (node.hasAttribute("data-ssn") || looksLikeSsn(text)) {
|
|
111
|
+
return redactSsn(text);
|
|
106
112
|
}
|
|
107
113
|
else if (node.hasAttribute("data-dob")) {
|
|
108
|
-
return
|
|
114
|
+
return redactDob(text);
|
|
109
115
|
}
|
|
110
116
|
return "*".repeat(text.length);
|
|
111
117
|
}
|
|
@@ -415,6 +421,12 @@ backendApi, apiKey, sessionId, envValue, deferRecordingStart = true, useWsWorker
|
|
|
415
421
|
blockSelector,
|
|
416
422
|
maskTextClass: captureSettings.maskTextClass ?? MASK_CLASS,
|
|
417
423
|
recordDOM: false, // after spread so it can't be overridden
|
|
424
|
+
// rrweb's legacy webp-snapshot canvas path is ALWAYS off — the
|
|
425
|
+
// Sailfish pixel recorder (gated by captureSettings.recordCanvas,
|
|
426
|
+
// below) is the only canvas capture. Hardcoded false AFTER the spread
|
|
427
|
+
// so the `recordCanvas` flag (which drives the Sailfish pipeline) can
|
|
428
|
+
// never re-enable rrweb's own canvas snapshotting.
|
|
429
|
+
recordCanvas: false,
|
|
418
430
|
});
|
|
419
431
|
const snapshotStartTime = Date.now();
|
|
420
432
|
const serializedDoc = await chunkedSnapshot(document, mirror, {
|
|
@@ -486,8 +498,105 @@ backendApi, apiKey, sessionId, envValue, deferRecordingStart = true, useWsWorker
|
|
|
486
498
|
maskTextSelector,
|
|
487
499
|
blockSelector,
|
|
488
500
|
maskTextClass: captureSettings.maskTextClass ?? MASK_CLASS,
|
|
501
|
+
// rrweb's legacy webp-snapshot canvas path is ALWAYS off (after the
|
|
502
|
+
// spread so a server `recordCanvas` flag can't re-enable it). The
|
|
503
|
+
// Sailfish pixel recorder below is the only canvas capture.
|
|
504
|
+
recordCanvas: false,
|
|
489
505
|
});
|
|
490
506
|
}
|
|
507
|
+
// ── Sailfish canvas recording (SFCV pipeline) — opt-in ──────────────
|
|
508
|
+
// Dynamic import keeps the capture core out of the main bundle path.
|
|
509
|
+
if (captureSettings.recordCanvas) {
|
|
510
|
+
try {
|
|
511
|
+
const { startCanvasRecording, stopCanvasRecording, flushAllCanvas, isCanvasRecordingActive, } = await import("./canvas/canvasManager");
|
|
512
|
+
// Same deps object is reused for the initial start AND for re-arming
|
|
513
|
+
// after a bfcache restore (A35), so settings/deps stay identical.
|
|
514
|
+
const canvasDeps = {
|
|
515
|
+
// A13: forward the full privacy framework into the canvas pipeline
|
|
516
|
+
// so canvas masking honors the SAME selectors/flags as rrweb DOM
|
|
517
|
+
// masking, plus the content-detection flags. (HIPAA is NOT a canvas
|
|
518
|
+
// suppression signal — it routes PHI downstream; canvas still
|
|
519
|
+
// records, devs use the mask/block selectors to exclude content.)
|
|
520
|
+
settings: {
|
|
521
|
+
maskCanvasClass: captureSettings.maskCanvasClass,
|
|
522
|
+
maskTextClass: captureSettings.maskTextClass,
|
|
523
|
+
maskTextSelector: captureSettings.maskTextSelector,
|
|
524
|
+
blockSelector: captureSettings.blockSelector,
|
|
525
|
+
unmaskSelector: captureSettings.unmaskSelector,
|
|
526
|
+
// P1/masking-P2: global canvas text masking + redaction style.
|
|
527
|
+
// maskCanvasText unset → mirror-DOM default (mask canvas text when DOM
|
|
528
|
+
// text masking is configured). The backend setting is a NULLABLE
|
|
529
|
+
// tri-state Boolean; `?? undefined` maps GraphQL null → unset so the
|
|
530
|
+
// classifier's mirror-DOM default still applies, while preserving an
|
|
531
|
+
// explicit `false` (opt-out) and `true` (force). canvasRedactStyle
|
|
532
|
+
// defaults to asterisks + color.
|
|
533
|
+
maskCanvasText: captureSettings.maskCanvasText ?? undefined,
|
|
534
|
+
canvasRedactStyle: captureSettings.canvasRedactStyle,
|
|
535
|
+
recordSsn: captureSettings.recordSsn,
|
|
536
|
+
recordCreditCardInfo: captureSettings.recordCreditCardInfo,
|
|
537
|
+
recordDob: captureSettings.recordDob,
|
|
538
|
+
recordRealName: captureSettings.recordRealName,
|
|
539
|
+
recordPassword: captureSettings.recordPassword,
|
|
540
|
+
},
|
|
541
|
+
mirror: record.mirror,
|
|
542
|
+
addSailfishEvent: (payload) => {
|
|
543
|
+
_record?.addSailfishEvent(EventType.SailfishCustom, {
|
|
544
|
+
...payload,
|
|
545
|
+
tag: "sf-canvas",
|
|
546
|
+
});
|
|
547
|
+
},
|
|
548
|
+
sendBinary: wsSendBinary,
|
|
549
|
+
getContextIds: () => ({
|
|
550
|
+
session_id: sessionId,
|
|
551
|
+
page_visit_uuid: getUrlAndStoredUuids().page_visit_uuid,
|
|
552
|
+
}),
|
|
553
|
+
// A11 transport backpressure: pause canvas video frame production
|
|
554
|
+
// while the WebSocket send buffer is saturated.
|
|
555
|
+
isTransportSaturated,
|
|
556
|
+
};
|
|
557
|
+
startCanvasRecording(canvasDeps);
|
|
558
|
+
// A23: NON-terminal flush on visibilitychange:"hidden". On mobile a
|
|
559
|
+
// tab/app switch fires visibilitychange(hidden) and the OS may kill
|
|
560
|
+
// the tab WITHOUT a reliable pagehide, losing the last ≤1s command
|
|
561
|
+
// batch + encoder tail. Flush (but do NOT stop) so recording resumes
|
|
562
|
+
// if the tab returns. Wrapped so it never throws into host-app code.
|
|
563
|
+
const onVisibilityHidden = () => {
|
|
564
|
+
try {
|
|
565
|
+
if (document.visibilityState === "hidden") {
|
|
566
|
+
void flushAllCanvas();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
/* never break the host app */
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
document.addEventListener("visibilitychange", onVisibilityHidden);
|
|
574
|
+
// Recording ends with the page — flush batches, final keyframes,
|
|
575
|
+
// op:"stop" events (stopCanvasRecording is idempotent). On bfcache
|
|
576
|
+
// entry (pagehide persisted) this still stops; pageshow re-arms.
|
|
577
|
+
window.addEventListener("pagehide", () => {
|
|
578
|
+
void stopCanvasRecording();
|
|
579
|
+
});
|
|
580
|
+
// A35: re-arm on bfcache restore. When the page enters bfcache the
|
|
581
|
+
// pagehide above stops canvas recording (terminal); without this the
|
|
582
|
+
// canvas stays off for the rest of the restored page's life even
|
|
583
|
+
// though the rest of the SDK keeps running. On pageshow(persisted),
|
|
584
|
+
// restart via the SAME start path. Guard against double-start.
|
|
585
|
+
window.addEventListener("pageshow", (event) => {
|
|
586
|
+
try {
|
|
587
|
+
if (event.persisted && !isCanvasRecordingActive()) {
|
|
588
|
+
startCanvasRecording(canvasDeps);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch (rearmError) {
|
|
592
|
+
console.warn("[Sailfish] canvas recording failed to re-arm after bfcache restore:", rearmError);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
catch (canvasError) {
|
|
597
|
+
console.warn("[Sailfish] canvas recording failed to start:", canvasError);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
491
600
|
};
|
|
492
601
|
if (deferRecordingStart) {
|
|
493
602
|
// Deterministic deferral: start heavy work only after page load completes,
|
package/dist/recording.js.br
CHANGED
|
Binary file
|
package/dist/recording.js.gz
CHANGED
|
Binary file
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 — framework semantic adapters (contract §9).
|
|
3
|
+
*
|
|
4
|
+
* Canvas pixels know WHERE a click landed (the pick buffer resolves that to a
|
|
5
|
+
* draw command / source line). Frameworks additionally know WHAT logical
|
|
6
|
+
* object the user clicked: a Konva "Upgrade CTA" shape, a Chart.js datapoint,
|
|
7
|
+
* a Three.js mesh. These adapters capture that semantic target on pointerdown
|
|
8
|
+
* and emit it as a `sf-canvas` op:"hit" index event:
|
|
9
|
+
*
|
|
10
|
+
* {tag:"sf-canvas", op:"hit", canvasId, t, x, y,
|
|
11
|
+
* target:{framework, label, objectType?, path?}}
|
|
12
|
+
*
|
|
13
|
+
* The SDK cannot import the customer's framework (three/konva/fabric/pixi/
|
|
14
|
+
* chart.js), so the customer (or a future build plugin) registers their live
|
|
15
|
+
* instance via the exported register* helpers below — each returns an
|
|
16
|
+
* unregister fn for component teardown. A window-global fallback covers
|
|
17
|
+
* script-tag (UMD) usage where the framework publishes a global.
|
|
18
|
+
*
|
|
19
|
+
* Every adapter callback is try/catch'd: a framework hiccup never breaks
|
|
20
|
+
* recording. SSR-safe: no window/document access at module scope.
|
|
21
|
+
*/
|
|
22
|
+
export interface SemanticTarget {
|
|
23
|
+
framework: "konva" | "fabric" | "chartjs" | "three" | "pixi";
|
|
24
|
+
/** human/logical label: Konva name|id, Chart "dataset[i]", three mesh name, ... */
|
|
25
|
+
label: string;
|
|
26
|
+
/** framework object type (Konva className, fabric type, chart type, ...) */
|
|
27
|
+
objectType?: string;
|
|
28
|
+
/** optional path/uuid for deeper identity */
|
|
29
|
+
path?: string;
|
|
30
|
+
/** [creation-site stamping — Layer A & B] build-time stamp id of the object's
|
|
31
|
+
* creation site; resolves to file:line via the EXISTING stamp manifest.
|
|
32
|
+
* Layer A stamps `new X()` sites; Layer B stamps object-PRODUCING calls
|
|
33
|
+
* (`load`/`loadAsync`/`parse`/`parseAsync`/`clone`/`copy`) — the build rewrites
|
|
34
|
+
* them to `__sfcreate(() => EXPR.method(args), id)`, whose helper calls
|
|
35
|
+
* `globalThis.__sfCreateFrom(result, id)` so the produced object subtree is
|
|
36
|
+
* marked with the producer's call-site id (see markSubtree below). */
|
|
37
|
+
createId?: string;
|
|
38
|
+
/** @deprecated Layer-B scaffolding field, no longer populated or read (build-time
|
|
39
|
+
* marking replaced runtime stack capture). Retained only so player/payload types
|
|
40
|
+
* that reference it do not break; SDK code never sets or reads it. */
|
|
41
|
+
createStack?: string[];
|
|
42
|
+
/** [Layer D — CAPTURE ONLY; backend resolve is phased] Chart.js datum position.
|
|
43
|
+
* Today the chart instance's createId (its `new Chart(...)` site) resolves the
|
|
44
|
+
* click; configRef is for a future dataset-definition resolver. */
|
|
45
|
+
configRef?: {
|
|
46
|
+
datasetIndex: number;
|
|
47
|
+
index?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Konva: hit-test the stage's hit graph; label from node name()/id()/class. */
|
|
51
|
+
export declare function registerKonvaStage(stage: unknown): () => void;
|
|
52
|
+
/** Fabric: findTarget(ev) on the canvas; label from object name/id/type. */
|
|
53
|
+
export declare function registerFabricCanvas(fabricCanvas: unknown): () => void;
|
|
54
|
+
/** Chart.js: getElementsAtEventForMode -> "datasetLabel[index]". */
|
|
55
|
+
export declare function registerChart(chart: unknown): () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Three.js: raycast on pointerdown. The customer passes their THREE namespace
|
|
58
|
+
* (the SDK must not bundle three) plus the scene/camera/renderer.
|
|
59
|
+
*/
|
|
60
|
+
export declare function registerThreeScene(scene: unknown, camera: unknown, renderer: unknown, three: unknown): () => void;
|
|
61
|
+
/** PixiJS (v8): hit-test the event boundary; label from object label/name. */
|
|
62
|
+
export declare function registerPixiApp(app: unknown): () => void;
|
|
63
|
+
export interface AdapterDeps {
|
|
64
|
+
/** recorded canvas -> its canvasId (null if not recorded) */
|
|
65
|
+
canvasIdFor: (canvas: HTMLCanvasElement) => string | null;
|
|
66
|
+
/** emit a sf-canvas index event (tag added downstream) */
|
|
67
|
+
emitHit: (payload: Record<string, unknown>) => void;
|
|
68
|
+
now: () => number;
|
|
69
|
+
}
|
|
70
|
+
export declare function installAdapters(d: AdapterDeps): void;
|
|
71
|
+
export declare function uninstallAdapters(): void;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { CommandEncoder } from "./codec";
|
|
2
|
+
import { CanvasGovernor, type DemoteReason } from "./governor";
|
|
3
|
+
import { KeyframeCapturer } from "./keyframes";
|
|
4
|
+
import { type ContextRecorder } from "./patch2d";
|
|
5
|
+
import { type GLRecorder } from "./patchGL";
|
|
6
|
+
import { type GPURecorderHandle } from "./patchWebGPU";
|
|
7
|
+
import { MediaRecorderCapture } from "./mediaRecorderCapture";
|
|
8
|
+
import { VideoCapture } from "./videoCapture";
|
|
9
|
+
import { type CanvasPrivacyMode, type RedactStyle } from "./canvasPrivacy";
|
|
10
|
+
/** [audit B3] Decay a leaky-bucket byte counter by `elapsedMs` at a drain rate of
|
|
11
|
+
* `budget / windowMs` bytes/ms (never below 0). Exported for tests. */
|
|
12
|
+
export declare function decayBytes(recentBytes: number, budget: number, windowMs: number, elapsedMs: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* A25 — global cap on simultaneously active hardware video encoders. Each
|
|
15
|
+
* video-mode canvas spins its own `VideoEncoder` (one codec session per
|
|
16
|
+
* encoder); a page with many heavy canvases can exhaust the OS's limited pool
|
|
17
|
+
* of codec sessions, after which every further `configure()` fails and ALL
|
|
18
|
+
* video capture silently dies. We cap the number of concurrent video-mode
|
|
19
|
+
* controllers PER SESSION: a canvas that would demote to video while the cap is
|
|
20
|
+
* reached stays on the KEYFRAME tier instead (degrade, don't fail). A slot frees
|
|
21
|
+
* when a video controller stops / tears down, and the next demotion can claim it.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MAX_CONCURRENT_VIDEO_CANVASES = 4;
|
|
24
|
+
/**
|
|
25
|
+
* A25 — the per-session pool of concurrent video-encoder slots. A fresh pool is
|
|
26
|
+
* created in each `startCanvasRecording` (so the cap is per-session, not global
|
|
27
|
+
* across the page's lifetime). `acquire` hands out at most `max` slots;
|
|
28
|
+
* `release` returns one and is idempotent at the boundaries (never drops below
|
|
29
|
+
* zero). The manager tracks per-controller ownership separately so a double
|
|
30
|
+
* teardown cannot over-release.
|
|
31
|
+
*/
|
|
32
|
+
export declare class VideoSlotPool {
|
|
33
|
+
private max;
|
|
34
|
+
private used;
|
|
35
|
+
constructor(max?: number);
|
|
36
|
+
/** @returns true if a slot was available and claimed. */
|
|
37
|
+
acquire(): boolean;
|
|
38
|
+
release(): void;
|
|
39
|
+
get inUse(): number;
|
|
40
|
+
get available(): number;
|
|
41
|
+
}
|
|
42
|
+
export interface CanvasRecorderSettings {
|
|
43
|
+
/** canvases matching `.${maskCanvasClass}`, `[data-sf-mask]`, or inside a
|
|
44
|
+
* masked subtree record NOTHING (default "sailfishSanitize") */
|
|
45
|
+
maskCanvasClass?: string;
|
|
46
|
+
/** rrweb maskTextClass — canvases under it REDACT drawn text (full mask) */
|
|
47
|
+
maskTextClass?: string;
|
|
48
|
+
/** rrweb maskTextSelector — canvases matching it REDACT drawn text */
|
|
49
|
+
maskTextSelector?: string;
|
|
50
|
+
/** rrweb blockSelector — canvases under it record NOTHING (suppress) */
|
|
51
|
+
blockSelector?: string;
|
|
52
|
+
/** rrweb unmaskSelector — OVERRIDE: forces normal recording (wins over block/mask) */
|
|
53
|
+
unmaskSelector?: string;
|
|
54
|
+
/**
|
|
55
|
+
* P1 — global canvas text masking. `true` redacts drawn text on every recorded
|
|
56
|
+
* canvas; `false` opts out of the mirror-DOM default; unset = mirror-DOM (if DOM
|
|
57
|
+
* text masking is configured, canvas text is masked too). See
|
|
58
|
+
* `CanvasPrivacySettings.maskCanvasText`.
|
|
59
|
+
*/
|
|
60
|
+
maskCanvasText?: boolean;
|
|
61
|
+
/** P1 — global redaction style (text asterisks|bar, shape color|block|remove).
|
|
62
|
+
* Defaults to {text:"asterisks", shape:"color"}; per-call `maskCanvasDraws`
|
|
63
|
+
* options override it. */
|
|
64
|
+
canvasRedactStyle?: Partial<RedactStyle>;
|
|
65
|
+
recordSsn?: boolean;
|
|
66
|
+
recordCreditCardInfo?: boolean;
|
|
67
|
+
recordDob?: boolean;
|
|
68
|
+
recordRealName?: boolean;
|
|
69
|
+
recordPassword?: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface CanvasRecorderDeps {
|
|
72
|
+
settings: CanvasRecorderSettings;
|
|
73
|
+
/** rrweb mirror — canvasId is the mirror node id at start (contract §1) */
|
|
74
|
+
mirror: {
|
|
75
|
+
getId(node: Node): number;
|
|
76
|
+
};
|
|
77
|
+
/** emits an index event through the rrweb pipeline; the caller adds
|
|
78
|
+
* tag:"sf-canvas" + EventType.SailfishCustom (contract §2) */
|
|
79
|
+
addSailfishEvent: (payload: Record<string, unknown>) => void;
|
|
80
|
+
/** ships one SFCV frame (websocket wsSendBinary) */
|
|
81
|
+
sendBinary: (buf: ArrayBuffer) => void;
|
|
82
|
+
/** current session/page-visit identifiers for frame headers */
|
|
83
|
+
getContextIds: () => {
|
|
84
|
+
session_id?: string | null;
|
|
85
|
+
page_visit_uuid?: string | null;
|
|
86
|
+
};
|
|
87
|
+
/** A11 backpressure: true when the transport's send buffer is saturated, so
|
|
88
|
+
* video capture pauses producing frames until it drains. Optional — when
|
|
89
|
+
* absent (or omitted by the host wiring), frame production is never throttled. */
|
|
90
|
+
isTransportSaturated?: () => boolean;
|
|
91
|
+
}
|
|
92
|
+
type Mode = "commands" | "video" | "none";
|
|
93
|
+
interface WorkerDoneMsg {
|
|
94
|
+
type: "sf-record-done";
|
|
95
|
+
batches: Uint8Array[];
|
|
96
|
+
chunks: {
|
|
97
|
+
tMs: number;
|
|
98
|
+
bytes: number;
|
|
99
|
+
}[];
|
|
100
|
+
cpuMs: number;
|
|
101
|
+
notes: string[];
|
|
102
|
+
assets: {
|
|
103
|
+
id: number;
|
|
104
|
+
kind: "image" | "imagedata";
|
|
105
|
+
t: number;
|
|
106
|
+
bytes: number;
|
|
107
|
+
w?: number;
|
|
108
|
+
h?: number;
|
|
109
|
+
bitmap?: ImageBitmap;
|
|
110
|
+
imageData?: ImageData;
|
|
111
|
+
}[];
|
|
112
|
+
}
|
|
113
|
+
interface Controller {
|
|
114
|
+
canvas: HTMLCanvasElement;
|
|
115
|
+
canvasId: string;
|
|
116
|
+
contextType: string;
|
|
117
|
+
mode: Mode;
|
|
118
|
+
/**
|
|
119
|
+
* A13 — privacy mode for this canvas. "normal" records as usual (with
|
|
120
|
+
* content-detection on drawn text); "redact" masks ALL drawn text AND
|
|
121
|
+
* suppresses pixel capture (keyframes/video) so rendered text never leaves
|
|
122
|
+
* the browser, and never demotes to video. ("suppress" canvases never get a
|
|
123
|
+
* controller — they are torn down / never built.) Carried on the controller
|
|
124
|
+
* so A6 re-eval can detect a privacy-mode TRANSITION mid-session.
|
|
125
|
+
*/
|
|
126
|
+
privacyMode: CanvasPrivacyMode;
|
|
127
|
+
seq: number;
|
|
128
|
+
dirty: boolean;
|
|
129
|
+
everDirty: boolean;
|
|
130
|
+
pumpScheduled: boolean;
|
|
131
|
+
flushTimer: number | null;
|
|
132
|
+
stopped: boolean;
|
|
133
|
+
demoted: boolean;
|
|
134
|
+
/** A25: this controller currently holds one of the global video-encoder
|
|
135
|
+
* slots. Set when it acquires a slot (demotion / worker-video), cleared when
|
|
136
|
+
* the slot is released (teardown / stop / video-start failure). Makes slot
|
|
137
|
+
* release idempotent so a double teardown never under-counts the pool. */
|
|
138
|
+
hasVideoSlot: boolean;
|
|
139
|
+
/** A10: GPU context (WebGL/WebGPU) is currently lost — drop all capture until
|
|
140
|
+
* it is restored, so we never record garbage from a dead context. */
|
|
141
|
+
contextLost: boolean;
|
|
142
|
+
/** A40: a tainted (cross-origin) source was drawn — emit op:"taint" once. */
|
|
143
|
+
taintEmitted: boolean;
|
|
144
|
+
/** A24: last canvas dimensions emitted via op:"start"/op:"resize" — a change
|
|
145
|
+
* mid-session re-emits dims + DPR and forces a keyframe (command/keyframe
|
|
146
|
+
* mode) so the player learns the new size. */
|
|
147
|
+
lastW: number;
|
|
148
|
+
lastH: number;
|
|
149
|
+
/** A10: remove the webglcontextlost/restored (or device.lost) listeners on
|
|
150
|
+
* teardown. */
|
|
151
|
+
removeContextLossListeners: (() => void) | null;
|
|
152
|
+
governor: CanvasGovernor;
|
|
153
|
+
encoder: CommandEncoder | null;
|
|
154
|
+
batchStart: number;
|
|
155
|
+
flushChain: Promise<void>;
|
|
156
|
+
keyframes: KeyframeCapturer | null;
|
|
157
|
+
video: VideoCapture | null;
|
|
158
|
+
/** A41: MediaRecorder fallback capture (Safari < 26: MediaRecorder present,
|
|
159
|
+
* WebCodecs absent). Mutually exclusive with `video` — a video-mode canvas
|
|
160
|
+
* uses exactly one encoder backend. */
|
|
161
|
+
mediaRecorder: MediaRecorderCapture | null;
|
|
162
|
+
recorder2d: ContextRecorder | null;
|
|
163
|
+
recorderGL: GLRecorder | null;
|
|
164
|
+
recorderGPU: GPURecorderHandle | null;
|
|
165
|
+
worker: Worker | null;
|
|
166
|
+
workerDone: Promise<WorkerDoneMsg> | null;
|
|
167
|
+
removeWorkerListener: (() => void) | null;
|
|
168
|
+
/**
|
|
169
|
+
* maskCanvasDraws: set once the app has wrapped any draw in `maskCanvasDraws`.
|
|
170
|
+
* Pixel capture (keyframes + video) is torn down because those masked draws
|
|
171
|
+
* painted real pixels; replay falls back to the sanitized command stream.
|
|
172
|
+
*/
|
|
173
|
+
pixelSuppressed: boolean;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A13 — project the recorder settings onto the classifier's privacy-settings
|
|
177
|
+
* shape. Empty-string selectors (the backend's "rule disabled" sentinel) are
|
|
178
|
+
* coerced to undefined so closestSafe/matchesSelectorSafe treat them as "no
|
|
179
|
+
* rule" rather than matching the empty selector.
|
|
180
|
+
*/
|
|
181
|
+
/**
|
|
182
|
+
* [audit B7] Return the NAMES of configured privacy selectors that are malformed
|
|
183
|
+
* (throw in `document.querySelector`). The matchers (closestSafe/
|
|
184
|
+
* matchesSelectorSafe) fail SAFE — a selector that throws in `el.matches()`/
|
|
185
|
+
* `el.closest()` simply doesn't match, so a typo in a block/mask/unmask selector
|
|
186
|
+
* silently DISABLES that rule (under-masking) with no signal. Surfacing the bad
|
|
187
|
+
* selector names in the recorder notes warns the operator without changing the
|
|
188
|
+
* fail-safe runtime behavior. SSR-guarded (no document on the server → []).
|
|
189
|
+
* Pure + exported so it's testable without the module's session state.
|
|
190
|
+
*/
|
|
191
|
+
export declare function collectMalformedSelectors(settings: CanvasRecorderSettings): string[];
|
|
192
|
+
export declare function startCanvasRecording(deps: CanvasRecorderDeps): void;
|
|
193
|
+
/** [audit C1] Await a settle() but never longer than `ms`, and never reject — a
|
|
194
|
+
* stuck/failed final encode must not hang or abort the rest of teardown. A late
|
|
195
|
+
* asset that resolves after the timeout still ships best-effort (or is dropped by
|
|
196
|
+
* sendFrame's `!state` guard once the session is fully gone). */
|
|
197
|
+
export declare function settleOrTimeout(p: Promise<void>, ms: number): Promise<unknown>;
|
|
198
|
+
/** Flush + final keyframes + op:"stop" per canvas; idempotent. Prototype
|
|
199
|
+
* patches stay installed but inert (contract §5). */
|
|
200
|
+
export declare function stopCanvasRecording(): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* A23: NON-terminal flush — flush every controller's pending command batch
|
|
203
|
+
* (the same flush the BATCH_MS timer / flushBatch does) and request each active
|
|
204
|
+
* video encoder to drain its queued frames. Recording stays armed and resumes
|
|
205
|
+
* normally if the page returns.
|
|
206
|
+
*
|
|
207
|
+
* Wired to visibilitychange:"hidden": on mobile a tab/app switch fires
|
|
208
|
+
* visibilitychange(hidden) and the OS may kill the tab WITHOUT a reliable
|
|
209
|
+
* pagehide, losing the last ≤1s command batch and the encoder tail. This pushes
|
|
210
|
+
* that data out while the page is still alive. Idempotent and safe to call
|
|
211
|
+
* repeatedly; a no-op when recording is inactive.
|
|
212
|
+
*/
|
|
213
|
+
export declare function flushAllCanvas(): Promise<void>;
|
|
214
|
+
/** Test/diagnostic hook. */
|
|
215
|
+
export declare function isCanvasRecordingActive(): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* A25 — test-only hooks for the concurrent-video-encoder cap. Exposes the real
|
|
218
|
+
* `demote` decision path and slot accounting so a unit test can drive N+1
|
|
219
|
+
* demotions deterministically WITHOUT spawning hardware encoders (the test
|
|
220
|
+
* stubs `VideoCapture`). NOT part of the public SDK surface — tree-shaken from
|
|
221
|
+
* production bundles since nothing in the app imports it. Each helper is a thin
|
|
222
|
+
* wrapper over the same internal functions the live discovery path uses, so the
|
|
223
|
+
* test exercises production behavior, not a parallel reimplementation.
|
|
224
|
+
*/
|
|
225
|
+
export declare const __canvasManagerTestHooks: {
|
|
226
|
+
/** Spin up a minimal session state (no DOM scan / hooks) for slot testing. */
|
|
227
|
+
initState(deps: CanvasRecorderDeps): void;
|
|
228
|
+
/** Register a synthetic command-mode 2D controller (no recorder/keyframes). */
|
|
229
|
+
addController(canvas: HTMLCanvasElement, canvasId: string, privacyMode?: CanvasPrivacyMode): Controller;
|
|
230
|
+
/** Classify a canvas against the current session's privacy settings. */
|
|
231
|
+
classify(canvas: HTMLCanvasElement): CanvasPrivacyMode;
|
|
232
|
+
/** Build the text redactor a controller in `mode` would install. */
|
|
233
|
+
makeTextRedactor(mode: CanvasPrivacyMode): ((text: string) => string) | undefined;
|
|
234
|
+
/** Run the real A6/A13 privacy re-evaluation pass. */
|
|
235
|
+
reevaluate(): void;
|
|
236
|
+
/** Escalation decision for pixel-text contexts (GL/GPU/worker). */
|
|
237
|
+
escalate(mode: CanvasPrivacyMode, contextType: string): CanvasPrivacyMode;
|
|
238
|
+
/** Whether a canvas is currently tracked by a controller. */
|
|
239
|
+
isTracked(canvas: HTMLCanvasElement): boolean;
|
|
240
|
+
/** Whether a canvas is in the suppressed (re-arm candidate) set. */
|
|
241
|
+
isSuppressed(canvas: HTMLCanvasElement): boolean;
|
|
242
|
+
/** Mark a canvas suppressed (as the discovery path would). */
|
|
243
|
+
markSuppressed(canvas: HTMLCanvasElement): void;
|
|
244
|
+
/** Read a controller's current privacy mode + whether keyframes are armed. */
|
|
245
|
+
controllerPrivacy(ctl: Controller): {
|
|
246
|
+
mode: CanvasPrivacyMode;
|
|
247
|
+
hasKeyframes: boolean;
|
|
248
|
+
};
|
|
249
|
+
/** Run the real maybeDemote path (respects the redact no-demote rule). */
|
|
250
|
+
tryDemote(ctl: Controller): void;
|
|
251
|
+
/** Run the real maybeEmitResize on a controller — drives the B1 resize +
|
|
252
|
+
* keyframeUnsafe leak path (suppress-before-keyframe ordering). */
|
|
253
|
+
emitResize(ctl: Controller): void;
|
|
254
|
+
/**
|
|
255
|
+
* Drive the REAL build2DController with a fake pre-attached recorder so the
|
|
256
|
+
* keyframe-suppression + redactor-installation behavior can be asserted
|
|
257
|
+
* without a live 2D context (jsdom has none). Returns the built controller
|
|
258
|
+
* plus the redactor the recorder received.
|
|
259
|
+
*/
|
|
260
|
+
build2DWithFakeRecorder(canvas: HTMLCanvasElement, canvasId: string, privacyMode: CanvasPrivacyMode): {
|
|
261
|
+
ctl: Controller | undefined;
|
|
262
|
+
redactor: ((t: string) => string) | undefined;
|
|
263
|
+
};
|
|
264
|
+
/** Run the real governor-demotion path on a controller. */
|
|
265
|
+
demote(ctl: Controller, reason?: DemoteReason): void;
|
|
266
|
+
/** Run the real (non-flushing) teardown, which frees the controller's slot. */
|
|
267
|
+
teardown(ctl: Controller): void;
|
|
268
|
+
/** Current per-session slot usage. */
|
|
269
|
+
slotsInUse(): number;
|
|
270
|
+
controllerHasSlot(ctl: Controller): boolean;
|
|
271
|
+
reset(): void;
|
|
272
|
+
};
|
|
273
|
+
export {};
|