@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.
Files changed (144) hide show
  1. package/README.md +8 -0
  2. package/dist/canvas/adapters.js +382 -0
  3. package/dist/canvas/adapters.js.br +0 -0
  4. package/dist/canvas/adapters.js.gz +0 -0
  5. package/dist/canvas/canvasManager.js +1949 -0
  6. package/dist/canvas/canvasManager.js.br +0 -0
  7. package/dist/canvas/canvasManager.js.gz +0 -0
  8. package/dist/canvas/canvasPrivacy.js +233 -0
  9. package/dist/canvas/canvasPrivacy.js.br +0 -0
  10. package/dist/canvas/canvasPrivacy.js.gz +0 -0
  11. package/dist/canvas/codec.js +314 -0
  12. package/dist/canvas/codec.js.br +0 -0
  13. package/dist/canvas/codec.js.gz +0 -0
  14. package/dist/canvas/deviceClass.js +67 -0
  15. package/dist/canvas/deviceClass.js.br +0 -0
  16. package/dist/canvas/deviceClass.js.gz +0 -0
  17. package/dist/canvas/encodeWorker.js +284 -0
  18. package/dist/canvas/encodeWorker.js.br +0 -0
  19. package/dist/canvas/encodeWorker.js.gz +0 -0
  20. package/dist/canvas/governor.js +79 -0
  21. package/dist/canvas/governor.js.br +0 -0
  22. package/dist/canvas/governor.js.gz +0 -0
  23. package/dist/canvas/keyframes.js +183 -0
  24. package/dist/canvas/keyframes.js.br +0 -0
  25. package/dist/canvas/keyframes.js.gz +0 -0
  26. package/dist/canvas/mediaRecorderCapture.js +257 -0
  27. package/dist/canvas/mediaRecorderCapture.js.br +0 -0
  28. package/dist/canvas/mediaRecorderCapture.js.gz +0 -0
  29. package/dist/canvas/objectLog.js +150 -0
  30. package/dist/canvas/objectLog.js.br +0 -0
  31. package/dist/canvas/objectLog.js.gz +0 -0
  32. package/dist/canvas/patch2d.js +1302 -0
  33. package/dist/canvas/patch2d.js.br +0 -0
  34. package/dist/canvas/patch2d.js.gz +0 -0
  35. package/dist/canvas/patchGL.js +371 -0
  36. package/dist/canvas/patchGL.js.br +0 -0
  37. package/dist/canvas/patchGL.js.gz +0 -0
  38. package/dist/canvas/patchWebGPU.js +361 -0
  39. package/dist/canvas/patchWebGPU.js.br +0 -0
  40. package/dist/canvas/patchWebGPU.js.gz +0 -0
  41. package/dist/canvas/piiDetect.js +155 -0
  42. package/dist/canvas/piiDetect.js.br +0 -0
  43. package/dist/canvas/piiDetect.js.gz +0 -0
  44. package/dist/canvas/protocol.js +122 -0
  45. package/dist/canvas/protocol.js.br +0 -0
  46. package/dist/canvas/protocol.js.gz +0 -0
  47. package/dist/canvas/videoCapture.js +252 -0
  48. package/dist/canvas/videoCapture.js.br +0 -0
  49. package/dist/canvas/videoCapture.js.gz +0 -0
  50. package/dist/canvas/workerRelay.js +99 -0
  51. package/dist/canvas/workerRelay.js.br +0 -0
  52. package/dist/canvas/workerRelay.js.gz +0 -0
  53. package/dist/canvas/workerShim.js +227 -0
  54. package/dist/canvas/workerShim.js.br +0 -0
  55. package/dist/canvas/workerShim.js.gz +0 -0
  56. package/dist/canvas-worker.cjs +74 -0
  57. package/dist/canvas-worker.cjs.br +0 -0
  58. package/dist/canvas-worker.cjs.gz +0 -0
  59. package/dist/canvas-worker.js +78 -0
  60. package/dist/canvas-worker.js.br +0 -0
  61. package/dist/canvas-worker.js.gz +0 -0
  62. package/dist/canvasFrameStore.js +198 -0
  63. package/dist/canvasFrameStore.js.br +0 -0
  64. package/dist/canvasFrameStore.js.gz +0 -0
  65. package/dist/chunks/canvasManager-BYn3_oyo.js +1440 -0
  66. package/dist/chunks/canvasManager-BYn3_oyo.js.br +0 -0
  67. package/dist/chunks/canvasManager-BYn3_oyo.js.gz +0 -0
  68. package/dist/chunks/canvasManager-CoX5072G.js +1456 -0
  69. package/dist/chunks/canvasManager-CoX5072G.js.br +0 -0
  70. package/dist/chunks/canvasManager-CoX5072G.js.gz +0 -0
  71. package/dist/chunks/{chunkSerializer-FQtY90Av.js → chunkSerializer-C_K0Wyk0.js} +1 -1
  72. package/dist/chunks/chunkSerializer-C_K0Wyk0.js.br +0 -0
  73. package/dist/chunks/chunkSerializer-C_K0Wyk0.js.gz +0 -0
  74. package/dist/chunks/{chunkSerializer-DDukZpgl.js → chunkSerializer-CghPitF4.js} +1 -1
  75. package/dist/chunks/chunkSerializer-CghPitF4.js.br +0 -0
  76. package/dist/chunks/chunkSerializer-CghPitF4.js.gz +0 -0
  77. package/dist/chunks/{index-C-qbsfKe.js → index-BIj4l3CV.js} +1028 -650
  78. package/dist/chunks/index-BIj4l3CV.js.br +0 -0
  79. package/dist/chunks/index-BIj4l3CV.js.gz +0 -0
  80. package/dist/chunks/{index-D6axlCRu.js → index-DIYomvdD.js} +1119 -720
  81. package/dist/chunks/index-DIYomvdD.js.br +0 -0
  82. package/dist/chunks/index-DIYomvdD.js.gz +0 -0
  83. package/dist/chunks/patch2d-9voowhJ_.js +816 -0
  84. package/dist/chunks/patch2d-9voowhJ_.js.br +0 -0
  85. package/dist/chunks/patch2d-9voowhJ_.js.gz +0 -0
  86. package/dist/chunks/patch2d-PQxcaE6r.js +886 -0
  87. package/dist/chunks/patch2d-PQxcaE6r.js.br +0 -0
  88. package/dist/chunks/patch2d-PQxcaE6r.js.gz +0 -0
  89. package/dist/graphql.js +12 -1
  90. package/dist/graphql.js.br +0 -0
  91. package/dist/graphql.js.gz +0 -0
  92. package/dist/inAppReportIssueModal/index.js +48 -0
  93. package/dist/inAppReportIssueModal/index.js.br +0 -0
  94. package/dist/inAppReportIssueModal/index.js.gz +0 -0
  95. package/dist/index.js +144 -2
  96. package/dist/index.js.br +0 -0
  97. package/dist/index.js.gz +0 -0
  98. package/dist/recorder.cjs +2 -2
  99. package/dist/recorder.cjs.br +0 -0
  100. package/dist/recorder.cjs.gz +0 -0
  101. package/dist/recorder.js +52 -41
  102. package/dist/recorder.js.br +0 -0
  103. package/dist/recorder.js.gz +0 -0
  104. package/dist/recorder.umd.cjs +4847 -2212
  105. package/dist/recorder.umd.cjs.br +0 -0
  106. package/dist/recorder.umd.cjs.gz +0 -0
  107. package/dist/recording.js +119 -10
  108. package/dist/recording.js.br +0 -0
  109. package/dist/recording.js.gz +0 -0
  110. package/dist/types/canvas/adapters.d.ts +71 -0
  111. package/dist/types/canvas/canvasManager.d.ts +273 -0
  112. package/dist/types/canvas/canvasPrivacy.d.ts +184 -0
  113. package/dist/types/canvas/codec.d.ts +27 -0
  114. package/dist/types/canvas/deviceClass.d.ts +40 -0
  115. package/dist/types/canvas/encodeWorker.d.ts +54 -0
  116. package/dist/types/canvas/governor.d.ts +44 -0
  117. package/dist/types/canvas/keyframes.d.ts +81 -0
  118. package/dist/types/canvas/mediaRecorderCapture.d.ts +91 -0
  119. package/dist/types/canvas/objectLog.d.ts +75 -0
  120. package/dist/types/canvas/patch2d.d.ts +339 -0
  121. package/dist/types/canvas/patchGL.d.ts +74 -0
  122. package/dist/types/canvas/patchWebGPU.d.ts +37 -0
  123. package/dist/types/canvas/piiDetect.d.ts +60 -0
  124. package/dist/types/canvas/protocol.d.ts +176 -0
  125. package/dist/types/canvas/videoCapture.d.ts +67 -0
  126. package/dist/types/canvas/workerRelay.d.ts +29 -0
  127. package/dist/types/canvas/workerShim.d.ts +10 -0
  128. package/dist/types/canvasFrameStore.d.ts +23 -0
  129. package/dist/types/graphql.d.ts +1 -1
  130. package/dist/types/index.d.ts +99 -1
  131. package/dist/types/types.d.ts +40 -0
  132. package/dist/types/websocket.d.ts +19 -0
  133. package/dist/websocket.js +210 -0
  134. package/dist/websocket.js.br +0 -0
  135. package/dist/websocket.js.gz +0 -0
  136. package/package.json +7 -1
  137. package/dist/chunks/chunkSerializer-DDukZpgl.js.br +0 -0
  138. package/dist/chunks/chunkSerializer-DDukZpgl.js.gz +0 -0
  139. package/dist/chunks/chunkSerializer-FQtY90Av.js.br +0 -0
  140. package/dist/chunks/chunkSerializer-FQtY90Av.js.gz +0 -0
  141. package/dist/chunks/index-C-qbsfKe.js.br +0 -0
  142. package/dist/chunks/index-C-qbsfKe.js.gz +0 -0
  143. package/dist/chunks/index-D6axlCRu.js.br +0 -0
  144. package/dist/chunks/index-D6axlCRu.js.gz +0 -0
Binary file
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
- const patterns = {
96
- creditCard: /\b(?:\d[ -]*?){13,16}\b/,
97
- ssn: /\b\d{3}-\d{2}-\d{4}\b/,
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
- patterns.creditCard.test(text)) {
102
- return "**** **** **** " + text.slice(-4);
107
+ looksLikeCreditCard(text)) {
108
+ return redactCreditCard(text);
103
109
  }
104
- else if (node.hasAttribute("data-ssn") || patterns.ssn.test(text)) {
105
- return "***-**-" + text.slice(-4);
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 "**/**/" + text.slice(-4);
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,
Binary file
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 {};