@anyreplay/electron 0.0.0-stage → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Baem Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
1
+ # @anyreplay/electron
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Session replay for Electron apps. Each window is recorded with
4
+ [`@anyreplay/browser`](../browser) — the same masking, transport and rrweb
5
+ setup as on the web — and reported as an Electron session with your app's id
6
+ and version.
7
+
8
+ ```bash
9
+ npm install @anyreplay/electron
10
+ ```
11
+
12
+ Three lines, one per Electron context.
13
+
14
+ ```ts
15
+ // main.ts — the main process
16
+ import { setupAnyReplay } from '@anyreplay/electron/main';
17
+ setupAnyReplay({ appId: 'com.example.notes' });
18
+ ```
19
+
20
+ ```ts
21
+ // preload.ts — bundled with your preload script
22
+ import { exposeAnyReplay } from '@anyreplay/electron/preload';
23
+ exposeAnyReplay();
24
+ ```
25
+
26
+ ```ts
27
+ // renderer — as early as you can in each window
28
+ import { init } from '@anyreplay/electron';
29
+ const replay = init({ projectKey: 'ar_pk_live_…' });
30
+ replay.track('note_opened');
31
+ ```
32
+
33
+ ## What each piece does
34
+
35
+ - **`setupAnyReplay(options)`** (main) answers the windows' request for the app
36
+ id and `app.getVersion()`; holds quitting back until every window has sent
37
+ what it recorded (`quitFlushTimeoutMs`, default 1500, `0` turns it off); and
38
+ forwards uncaught main-process exceptions into the focused window's session
39
+ as an error, recorded exactly as one the window caught itself would be
40
+ (`forwardMainErrors`, default on). It
41
+ observes them with `uncaughtExceptionMonitor`, so how your app handles them
42
+ does not change. Returns `{ dispose }`.
43
+ - **`exposeAnyReplay()`** (preload) puts `window.anyreplayElectron` on the page
44
+ through `contextBridge`: the app info, a flush the main process can ask for,
45
+ and the main-process errors. Nothing else — no generic IPC. Works with
46
+ `contextIsolation: true`, `sandbox: true` and no `nodeIntegration`
47
+ (Electron's defaults). A sandboxed preload cannot load `node_modules` at run
48
+ time, so bundle it.
49
+ - **`init(options)`** (renderer) takes every option of `@anyreplay/browser`,
50
+ plus `appId` and `appVersion` (normally left to the main process) and
51
+ `flushOnBlur` (default on). It returns at once; recording starts when the
52
+ app info arrives, and calls made before that are queued.
53
+
54
+ ## The app id
55
+
56
+ Electron has no API for your bundle id, so pass it to `setupAnyReplay` — the
57
+ `appId` from electron-builder, or Forge's `appBundleId`. Without it the id is
58
+ made from `app.getName()` (`"Acme Notes"` → `acme-notes`). A packaged window
59
+ loads from `file://` and has no web origin, so a project that lists allowed
60
+ apps admits it by this id.
61
+
62
+ ## Windows and sessions
63
+
64
+ Each `BrowserWindow` is its own session: the session id lives in the window's
65
+ `sessionStorage`. The visitor id lives in `localStorage`, which Electron keeps
66
+ across launches and shares between the windows of one session partition, so
67
+ one person is one visitor however many windows they open.
68
+
69
+ ## Content Security Policy
70
+
71
+ If your app sets a CSP, allow the ingest host in `connect-src`:
72
+
73
+ ```
74
+ connect-src 'self' https://in.anyreplay.com
75
+ ```
76
+
77
+ (or your self-hosted ingest URL).
78
+
79
+ ## Requirements
80
+
81
+ Electron 28 or later.
82
+
83
+ MIT © Baem Tech
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The contract between the three halves of the Electron SDK.
3
+ *
4
+ * The renderer records; the preload script is the only code that can talk to
5
+ * the main process; the main process knows the app's id and version and sees
6
+ * the app quit. They share nothing but these names, so each entry point can be
7
+ * imported on its own without pulling in the others (or the recorder, which
8
+ * has no business in the main process).
9
+ */
10
+ /** The global the preload script exposes through `contextBridge`. */
11
+ declare const BRIDGE_KEY = "anyreplayElectron";
12
+ /** IPC channels. All of them, so a reviewer can see exactly what crosses. */
13
+ declare const CHANNELS: {
14
+ /** renderer → main (invoke): `AppInfo`. */
15
+ readonly appInfo: "anyreplay:app-info";
16
+ /** main → renderer: flush now, then answer on `flushed` with the same id. */
17
+ readonly flush: "anyreplay:flush";
18
+ /** renderer → main: a flush asked for by `flush` has finished. */
19
+ readonly flushed: "anyreplay:flushed";
20
+ /** main → renderer: an uncaught exception in the main process. */
21
+ readonly mainError: "anyreplay:main-error";
22
+ };
23
+ /** What the main process says about the app. */
24
+ interface AppInfo {
25
+ /** The app id (§2.2 of the SDK contract), already checked against the id pattern. Absent when none could be found. */
26
+ appId?: string;
27
+ /** `app.getVersion()`. */
28
+ appVersion?: string;
29
+ }
30
+ /** An uncaught exception in the main process, as sent to a window. Raw: the renderer redacts it. */
31
+ interface MainErrorMessage {
32
+ name?: string;
33
+ message: string;
34
+ stack?: string;
35
+ }
36
+ /**
37
+ * What the preload script exposes on `window.anyreplayElectron`.
38
+ *
39
+ * Deliberately narrow: three fixed channels, no generic `send` or `invoke`, so
40
+ * exposing it gives page script no more reach into the main process than
41
+ * asking for the app's version and answering a flush.
42
+ */
43
+ interface AnyReplayBridge {
44
+ /** The bridge's own version, so a newer renderer can tell an older preload apart. */
45
+ readonly version: 1;
46
+ appInfo: () => Promise<AppInfo>;
47
+ /** The renderer's flush; called when the main process is about to quit. Replaces any earlier one. */
48
+ onFlushRequest: (flush: () => Promise<void> | void) => void;
49
+ /** Called with each main-process error forwarded to this window. Replaces any earlier listener. */
50
+ onMainError: (listener: (error: MainErrorMessage) => void) => void;
51
+ }
52
+
53
+ export { type AppInfo as A, BRIDGE_KEY as B, CHANNELS as C, type MainErrorMessage as M, type AnyReplayBridge as a };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The contract between the three halves of the Electron SDK.
3
+ *
4
+ * The renderer records; the preload script is the only code that can talk to
5
+ * the main process; the main process knows the app's id and version and sees
6
+ * the app quit. They share nothing but these names, so each entry point can be
7
+ * imported on its own without pulling in the others (or the recorder, which
8
+ * has no business in the main process).
9
+ */
10
+ /** The global the preload script exposes through `contextBridge`. */
11
+ declare const BRIDGE_KEY = "anyreplayElectron";
12
+ /** IPC channels. All of them, so a reviewer can see exactly what crosses. */
13
+ declare const CHANNELS: {
14
+ /** renderer → main (invoke): `AppInfo`. */
15
+ readonly appInfo: "anyreplay:app-info";
16
+ /** main → renderer: flush now, then answer on `flushed` with the same id. */
17
+ readonly flush: "anyreplay:flush";
18
+ /** renderer → main: a flush asked for by `flush` has finished. */
19
+ readonly flushed: "anyreplay:flushed";
20
+ /** main → renderer: an uncaught exception in the main process. */
21
+ readonly mainError: "anyreplay:main-error";
22
+ };
23
+ /** What the main process says about the app. */
24
+ interface AppInfo {
25
+ /** The app id (§2.2 of the SDK contract), already checked against the id pattern. Absent when none could be found. */
26
+ appId?: string;
27
+ /** `app.getVersion()`. */
28
+ appVersion?: string;
29
+ }
30
+ /** An uncaught exception in the main process, as sent to a window. Raw: the renderer redacts it. */
31
+ interface MainErrorMessage {
32
+ name?: string;
33
+ message: string;
34
+ stack?: string;
35
+ }
36
+ /**
37
+ * What the preload script exposes on `window.anyreplayElectron`.
38
+ *
39
+ * Deliberately narrow: three fixed channels, no generic `send` or `invoke`, so
40
+ * exposing it gives page script no more reach into the main process than
41
+ * asking for the app's version and answering a flush.
42
+ */
43
+ interface AnyReplayBridge {
44
+ /** The bridge's own version, so a newer renderer can tell an older preload apart. */
45
+ readonly version: 1;
46
+ appInfo: () => Promise<AppInfo>;
47
+ /** The renderer's flush; called when the main process is about to quit. Replaces any earlier one. */
48
+ onFlushRequest: (flush: () => Promise<void> | void) => void;
49
+ /** Called with each main-process error forwarded to this window. Replaces any earlier listener. */
50
+ onMainError: (listener: (error: MainErrorMessage) => void) => void;
51
+ }
52
+
53
+ export { type AppInfo as A, BRIDGE_KEY as B, CHANNELS as C, type MainErrorMessage as M, type AnyReplayBridge as a };
@@ -0,0 +1,18 @@
1
+ // src/bridge.ts
2
+ var BRIDGE_KEY = "anyreplayElectron";
3
+ var CHANNELS = {
4
+ /** renderer → main (invoke): `AppInfo`. */
5
+ appInfo: "anyreplay:app-info",
6
+ /** main → renderer: flush now, then answer on `flushed` with the same id. */
7
+ flush: "anyreplay:flush",
8
+ /** renderer → main: a flush asked for by `flush` has finished. */
9
+ flushed: "anyreplay:flushed",
10
+ /** main → renderer: an uncaught exception in the main process. */
11
+ mainError: "anyreplay:main-error"
12
+ };
13
+
14
+ export {
15
+ BRIDGE_KEY,
16
+ CHANNELS
17
+ };
18
+ //# sourceMappingURL=chunk-5TQ2HVGX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bridge.ts"],"sourcesContent":["/**\n * The contract between the three halves of the Electron SDK.\n *\n * The renderer records; the preload script is the only code that can talk to\n * the main process; the main process knows the app's id and version and sees\n * the app quit. They share nothing but these names, so each entry point can be\n * imported on its own without pulling in the others (or the recorder, which\n * has no business in the main process).\n */\n\n/** The global the preload script exposes through `contextBridge`. */\nexport const BRIDGE_KEY = 'anyreplayElectron';\n\n/** IPC channels. All of them, so a reviewer can see exactly what crosses. */\nexport const CHANNELS = {\n /** renderer → main (invoke): `AppInfo`. */\n appInfo: 'anyreplay:app-info',\n /** main → renderer: flush now, then answer on `flushed` with the same id. */\n flush: 'anyreplay:flush',\n /** renderer → main: a flush asked for by `flush` has finished. */\n flushed: 'anyreplay:flushed',\n /** main → renderer: an uncaught exception in the main process. */\n mainError: 'anyreplay:main-error',\n} as const;\n\n/** What the main process says about the app. */\nexport interface AppInfo {\n /** The app id (§2.2 of the SDK contract), already checked against the id pattern. Absent when none could be found. */\n appId?: string;\n /** `app.getVersion()`. */\n appVersion?: string;\n}\n\n/** An uncaught exception in the main process, as sent to a window. Raw: the renderer redacts it. */\nexport interface MainErrorMessage {\n name?: string;\n message: string;\n stack?: string;\n}\n\n/**\n * What the preload script exposes on `window.anyreplayElectron`.\n *\n * Deliberately narrow: three fixed channels, no generic `send` or `invoke`, so\n * exposing it gives page script no more reach into the main process than\n * asking for the app's version and answering a flush.\n */\nexport interface AnyReplayBridge {\n /** The bridge's own version, so a newer renderer can tell an older preload apart. */\n readonly version: 1;\n appInfo: () => Promise<AppInfo>;\n /** The renderer's flush; called when the main process is about to quit. Replaces any earlier one. */\n onFlushRequest: (flush: () => Promise<void> | void) => void;\n /** Called with each main-process error forwarded to this window. Replaces any earlier listener. */\n onMainError: (listener: (error: MainErrorMessage) => void) => void;\n}\n"],"mappings":";AAWO,IAAM,aAAa;AAGnB,IAAM,WAAW;AAAA;AAAA,EAEtB,SAAS;AAAA;AAAA,EAET,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,WAAW;AACb;","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ BRIDGE_KEY: () => BRIDGE_KEY,
24
+ CHANNELS: () => CHANNELS,
25
+ SDK_NAME: () => SDK_NAME,
26
+ SDK_VERSION: () => SDK_VERSION,
27
+ __resetForTests: () => __resetForTests,
28
+ electronSetup: () => electronSetup,
29
+ init: () => init,
30
+ mainError: () => mainError
31
+ });
32
+ module.exports = __toCommonJS(src_exports);
33
+
34
+ // src/bridge.ts
35
+ var BRIDGE_KEY = "anyreplayElectron";
36
+ var CHANNELS = {
37
+ /** renderer → main (invoke): `AppInfo`. */
38
+ appInfo: "anyreplay:app-info",
39
+ /** main → renderer: flush now, then answer on `flushed` with the same id. */
40
+ flush: "anyreplay:flush",
41
+ /** renderer → main: a flush asked for by `flush` has finished. */
42
+ flushed: "anyreplay:flushed",
43
+ /** main → renderer: an uncaught exception in the main process. */
44
+ mainError: "anyreplay:main-error"
45
+ };
46
+
47
+ // src/version.ts
48
+ var SDK_NAME = "@anyreplay/electron";
49
+ var SDK_VERSION = true ? "0.2.1" : "0.0.0-dev";
50
+ var PLATFORM = "electron";
51
+
52
+ // src/web-family.ts
53
+ var import_browser = require("@anyreplay/browser");
54
+ var MAX_QUEUED = 64;
55
+ function deferredRecorder(start, onStarted) {
56
+ let real = null;
57
+ let stopped = false;
58
+ const queue = [];
59
+ const run = (call) => {
60
+ if (real) {
61
+ call(real);
62
+ return;
63
+ }
64
+ if (stopped || queue.length >= MAX_QUEUED) return;
65
+ queue.push(call);
66
+ };
67
+ const ready = start().then((setup) => {
68
+ if (!setup || stopped) return;
69
+ real = (0, import_browser.createRecorder)(setup.options, setup.shell);
70
+ for (const call of queue.splice(0)) call(real);
71
+ onStarted?.(real);
72
+ }).catch((error) => {
73
+ if (typeof console !== "undefined") console.warn("[anyreplay]", error);
74
+ });
75
+ return {
76
+ ready,
77
+ status: () => real ? real.status() : stopped ? "stopped" : "idle",
78
+ sessionId: () => real?.sessionId() ?? null,
79
+ consent: (granted) => run((h) => h.consent(granted)),
80
+ identify: (traits) => run((h) => h.identify(traits)),
81
+ track: (name, properties) => run((h) => h.track(name, properties)),
82
+ trackError: (error) => run((h) => h.trackError(error)),
83
+ stop: () => {
84
+ if (real) real.stop();
85
+ else {
86
+ stopped = true;
87
+ queue.length = 0;
88
+ }
89
+ },
90
+ flush: async () => {
91
+ await ready;
92
+ await real?.flush();
93
+ }
94
+ };
95
+ }
96
+ var APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;
97
+ var clipVersion = (version) => {
98
+ const trimmed = version?.trim();
99
+ return trimmed ? trimmed.slice(0, 40) : void 0;
100
+ };
101
+
102
+ // src/index.ts
103
+ var current = null;
104
+ function init(options) {
105
+ if (current) return current;
106
+ const bridge = findBridge();
107
+ const { flushOnBlur = true, ...rest } = options;
108
+ const handle = deferredRecorder(
109
+ async () => {
110
+ const info = bridge ? await bridge.appInfo() : {};
111
+ return electronSetup(rest, info);
112
+ },
113
+ (recorder) => wire(recorder, bridge, flushOnBlur)
114
+ );
115
+ current = handle;
116
+ return handle;
117
+ }
118
+ function electronSetup(options, info) {
119
+ const appId = options.appId?.trim() || info.appId;
120
+ if (!appId && typeof console !== "undefined") {
121
+ console.warn("[anyreplay] no appId: call setupAnyReplay({ appId }) in the main process and exposeAnyReplay() in the preload. A project that lists allowed apps refuses sessions without one.");
122
+ }
123
+ if (appId && !APP_ID_PATTERN.test(appId)) {
124
+ throw new Error(`anyreplay: appId ${JSON.stringify(appId)} should be an app identifier such as com.example.app`);
125
+ }
126
+ const { appVersion: ownVersion, ...browserOptions } = options;
127
+ const appVersion = clipVersion(ownVersion ?? info.appVersion);
128
+ return {
129
+ options: {
130
+ // Inline unless the app says otherwise: a window loaded from the app's
131
+ // own files has addresses no reviewer can load (contract §9.1).
132
+ assets: "inline",
133
+ ...browserOptions,
134
+ ...appId ? { appId } : {}
135
+ },
136
+ shell: {
137
+ platform: PLATFORM,
138
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
139
+ ...appVersion ? { appVersion } : {}
140
+ }
141
+ };
142
+ }
143
+ function wire(recorder, bridge, flushOnBlur) {
144
+ bridge?.onFlushRequest(() => recorder.flush());
145
+ bridge?.onMainError((error) => recorder.trackError(mainError(error)));
146
+ if (flushOnBlur && typeof window !== "undefined") {
147
+ window.addEventListener("blur", () => {
148
+ void recorder.flush();
149
+ });
150
+ }
151
+ }
152
+ function mainError(error) {
153
+ const value = new Error(typeof error?.message === "string" ? error.message : "error");
154
+ value.name = typeof error?.name === "string" && error.name ? error.name : "Error";
155
+ value.stack = typeof error?.stack === "string" ? error.stack : "";
156
+ return value;
157
+ }
158
+ function findBridge() {
159
+ const candidate = globalThis[BRIDGE_KEY];
160
+ return candidate && typeof candidate.appInfo === "function" ? candidate : null;
161
+ }
162
+ function __resetForTests() {
163
+ current?.stop();
164
+ current = null;
165
+ }
166
+ // Annotate the CommonJS export names for ESM import in node:
167
+ 0 && (module.exports = {
168
+ BRIDGE_KEY,
169
+ CHANNELS,
170
+ SDK_NAME,
171
+ SDK_VERSION,
172
+ __resetForTests,
173
+ electronSetup,
174
+ init,
175
+ mainError
176
+ });
177
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/bridge.ts","../src/version.ts","../src/web-family.ts"],"sourcesContent":["import type { AnyReplayOptions, RecorderHandle } from '@anyreplay/browser';\nimport { BRIDGE_KEY, type AnyReplayBridge, type AppInfo, type MainErrorMessage } from './bridge.js';\nimport { PLATFORM, SDK_NAME, SDK_VERSION } from './version.js';\nimport { APP_ID_PATTERN, clipVersion, deferredRecorder, type DeferredHandle, type RecorderSetup } from './web-family.js';\n\nexport { BRIDGE_KEY, CHANNELS, type AnyReplayBridge, type AppInfo, type MainErrorMessage } from './bridge.js';\nexport { SDK_NAME, SDK_VERSION } from './version.js';\nexport type { RecorderSetup } from './web-family.js';\n\nexport interface AnyReplayElectronOptions extends AnyReplayOptions {\n /**\n * The app id. Normally left out: the main process supplies it\n * (`setupAnyReplay({ appId })`). Set here, it wins.\n */\n appId?: string;\n /** The app version. Normally left out: the main process supplies `app.getVersion()`. */\n appVersion?: string;\n /**\n * Send what has been recorded whenever the window loses focus. On by\n * default: a desktop app is rarely closed the way a tab is, and the main\n * process can be killed without `before-quit`.\n */\n flushOnBlur?: boolean;\n}\n\nexport type AnyReplayElectron = DeferredHandle;\n\nlet current: AnyReplayElectron | null = null;\n\n/**\n * Starts recording this window. Call it once in the renderer, as early as you\n * can:\n *\n * ```ts\n * import { init } from '@anyreplay/electron';\n * const replay = init({ projectKey: 'ar_pk_live_…' });\n * replay.track('document_opened');\n * ```\n *\n * The app id and version come from the main process through the preload\n * bridge (`exposeAnyReplay()` and `setupAnyReplay()`), so recording starts a\n * moment after this returns; calls made in between are queued. A second call\n * returns the first handle.\n *\n * Each window is its own session: the session id lives in the window's\n * `sessionStorage`, the visitor id in `localStorage`, which Electron keeps\n * across launches and shares between windows of the same session partition.\n */\nexport function init(options: AnyReplayElectronOptions): AnyReplayElectron {\n if (current) return current;\n const bridge = findBridge();\n const { flushOnBlur = true, ...rest } = options;\n\n const handle = deferredRecorder(\n async () => {\n const info: AppInfo = bridge ? await bridge.appInfo() : {};\n return electronSetup(rest, info);\n },\n (recorder) => wire(recorder, bridge, flushOnBlur),\n );\n current = handle;\n return handle;\n}\n\n/**\n * The browser SDK's options for this window — the installer's, with the app\n * id filled in — and the shell it runs in, which only the wrapper knows.\n */\nexport function electronSetup(options: AnyReplayElectronOptions, info: AppInfo): RecorderSetup {\n const appId = options.appId?.trim() || info.appId;\n if (!appId && typeof console !== 'undefined') {\n console.warn('[anyreplay] no appId: call setupAnyReplay({ appId }) in the main process and exposeAnyReplay() in the preload. A project that lists allowed apps refuses sessions without one.');\n }\n if (appId && !APP_ID_PATTERN.test(appId)) {\n // Thrown by the browser SDK too; said here in the shell's own terms.\n throw new Error(`anyreplay: appId ${JSON.stringify(appId)} should be an app identifier such as com.example.app`);\n }\n const { appVersion: ownVersion, ...browserOptions } = options;\n const appVersion = clipVersion(ownVersion ?? info.appVersion);\n return {\n options: {\n // Inline unless the app says otherwise: a window loaded from the app's\n // own files has addresses no reviewer can load (contract §9.1).\n assets: 'inline',\n ...browserOptions,\n ...(appId ? { appId } : {}),\n },\n shell: {\n platform: PLATFORM,\n sdk: { name: SDK_NAME, version: SDK_VERSION },\n ...(appVersion ? { appVersion } : {}),\n },\n };\n}\n\nfunction wire(recorder: RecorderHandle, bridge: AnyReplayBridge | null, flushOnBlur: boolean): void {\n bridge?.onFlushRequest(() => recorder.flush());\n // An error like the window's own: the same `anyreplay.error` event, the same\n // redaction and ceiling, and the session's `hasError` set (contract §8.3).\n bridge?.onMainError((error) => recorder.trackError(mainError(error)));\n if (flushOnBlur && typeof window !== 'undefined') {\n window.addEventListener('blur', () => { void recorder.flush(); });\n }\n}\n\n/**\n * A main-process error as an `Error` again — it crossed IPC as plain fields —\n * for the browser SDK to redact and clip like any other.\n */\nexport function mainError(error: MainErrorMessage | undefined): Error {\n const value = new Error(typeof error?.message === 'string' ? error.message : 'error');\n value.name = typeof error?.name === 'string' && error.name ? error.name : 'Error';\n value.stack = typeof error?.stack === 'string' ? error.stack : '';\n return value;\n}\n\nfunction findBridge(): AnyReplayBridge | null {\n const candidate = (globalThis as Record<string, unknown>)[BRIDGE_KEY] as AnyReplayBridge | undefined;\n return candidate && typeof candidate.appInfo === 'function' ? candidate : null;\n}\n\n/** For tests: forget the handle `init` returned. */\nexport function __resetForTests(): void {\n current?.stop();\n current = null;\n}\n","/**\n * The contract between the three halves of the Electron SDK.\n *\n * The renderer records; the preload script is the only code that can talk to\n * the main process; the main process knows the app's id and version and sees\n * the app quit. They share nothing but these names, so each entry point can be\n * imported on its own without pulling in the others (or the recorder, which\n * has no business in the main process).\n */\n\n/** The global the preload script exposes through `contextBridge`. */\nexport const BRIDGE_KEY = 'anyreplayElectron';\n\n/** IPC channels. All of them, so a reviewer can see exactly what crosses. */\nexport const CHANNELS = {\n /** renderer → main (invoke): `AppInfo`. */\n appInfo: 'anyreplay:app-info',\n /** main → renderer: flush now, then answer on `flushed` with the same id. */\n flush: 'anyreplay:flush',\n /** renderer → main: a flush asked for by `flush` has finished. */\n flushed: 'anyreplay:flushed',\n /** main → renderer: an uncaught exception in the main process. */\n mainError: 'anyreplay:main-error',\n} as const;\n\n/** What the main process says about the app. */\nexport interface AppInfo {\n /** The app id (§2.2 of the SDK contract), already checked against the id pattern. Absent when none could be found. */\n appId?: string;\n /** `app.getVersion()`. */\n appVersion?: string;\n}\n\n/** An uncaught exception in the main process, as sent to a window. Raw: the renderer redacts it. */\nexport interface MainErrorMessage {\n name?: string;\n message: string;\n stack?: string;\n}\n\n/**\n * What the preload script exposes on `window.anyreplayElectron`.\n *\n * Deliberately narrow: three fixed channels, no generic `send` or `invoke`, so\n * exposing it gives page script no more reach into the main process than\n * asking for the app's version and answering a flush.\n */\nexport interface AnyReplayBridge {\n /** The bridge's own version, so a newer renderer can tell an older preload apart. */\n readonly version: 1;\n appInfo: () => Promise<AppInfo>;\n /** The renderer's flush; called when the main process is about to quit. Replaces any earlier one. */\n onFlushRequest: (flush: () => Promise<void> | void) => void;\n /** Called with each main-process error forwarded to this window. Replaces any earlier listener. */\n onMainError: (listener: (error: MainErrorMessage) => void) => void;\n}\n","/**\n * Which SDK recorded a session (docs/SDK-CONTRACT.md §2.4).\n *\n * The version is written in at build time by tsup (and by Vitest for the\n * tests) from package.json, as in the browser SDK; source used without that\n * step reports `0.0.0-dev`.\n */\ndeclare const __ANYREPLAY_SDK_VERSION__: string | undefined;\n\nexport const SDK_NAME = '@anyreplay/electron';\n\nexport const SDK_VERSION: string =\n typeof __ANYREPLAY_SDK_VERSION__ === 'string' ? __ANYREPLAY_SDK_VERSION__ : '0.0.0-dev';\n\nexport const PLATFORM = 'electron';\n","import {\n createRecorder, type AnyReplayOptions, type RecorderHandle, type RecorderStatus, type ShellInfo,\n} from '@anyreplay/browser';\n\n/**\n * What a desktop shell hands the browser SDK: its options, and who is\n * recording (docs/SDK-CONTRACT.md §1, §4) — the platform and SDK the session\n * reports and the app's version. `createRecorder(options, shell)` is how a\n * wrapper names itself without a public option a website could set.\n */\nexport interface RecorderSetup {\n options: AnyReplayOptions;\n shell: ShellInfo;\n}\n\n/**\n * A recorder handle that exists before the recorder does.\n *\n * A shell learns its app id and version asynchronously (an IPC round trip in\n * Electron, a Tauri command), but `init` has to hand something back at once so\n * an app can call `track` on its next line. Calls made in between are queued\n * in order and replayed on the real handle; `flush` waits for it.\n */\nexport interface DeferredHandle extends RecorderHandle {\n /** Resolves once the recorder has started (or failed to). Never rejects. */\n ready: Promise<void>;\n}\n\ntype Queued = (handle: RecorderHandle) => void;\n\n/** At most this many calls wait for the app info; enough for a boot sequence. */\nconst MAX_QUEUED = 64;\n\nexport function deferredRecorder(\n start: () => Promise<RecorderSetup | null>,\n onStarted?: (handle: RecorderHandle) => void,\n): DeferredHandle {\n let real: RecorderHandle | null = null;\n let stopped = false;\n const queue: Queued[] = [];\n\n const run = (call: Queued): void => {\n if (real) {\n call(real);\n return;\n }\n if (stopped || queue.length >= MAX_QUEUED) return;\n queue.push(call);\n };\n\n const ready = start()\n .then((setup) => {\n if (!setup || stopped) return;\n real = createRecorder(setup.options, setup.shell);\n for (const call of queue.splice(0)) call(real);\n onStarted?.(real);\n })\n .catch((error: unknown) => {\n // Nothing this library does may surface as an error in the app.\n if (typeof console !== 'undefined') console.warn('[anyreplay]', error);\n });\n\n return {\n ready,\n status: (): RecorderStatus => (real ? real.status() : stopped ? 'stopped' : 'idle'),\n sessionId: () => real?.sessionId() ?? null,\n consent: (granted) => run((h) => h.consent(granted)),\n identify: (traits) => run((h) => h.identify(traits)),\n track: (name, properties) => run((h) => h.track(name, properties)),\n trackError: (error) => run((h) => h.trackError(error)),\n stop: () => {\n if (real) real.stop();\n else { stopped = true; queue.length = 0; }\n },\n flush: async () => {\n await ready;\n await real?.flush();\n },\n };\n}\n\n/** The browser SDK's app-id rule (`APP_ID_PATTERN`), so a bad id is caught here with a better message. */\nexport const APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;\n\n/** `meta.appVersion` is at most 40 characters (§2.5). */\nexport const clipVersion = (version: string | undefined): string | undefined => {\n const trimmed = version?.trim();\n return trimmed ? trimmed.slice(0, 40) : undefined;\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,80 @@
1
+ import { RecorderHandle, AnyReplayOptions, ShellInfo } from '@anyreplay/browser';
2
+ import { A as AppInfo, M as MainErrorMessage } from './bridge-BQKD7rrG.cjs';
3
+ export { a as AnyReplayBridge, B as BRIDGE_KEY, C as CHANNELS } from './bridge-BQKD7rrG.cjs';
4
+
5
+ /**
6
+ * What a desktop shell hands the browser SDK: its options, and who is
7
+ * recording (docs/SDK-CONTRACT.md §1, §4) — the platform and SDK the session
8
+ * reports and the app's version. `createRecorder(options, shell)` is how a
9
+ * wrapper names itself without a public option a website could set.
10
+ */
11
+ interface RecorderSetup {
12
+ options: AnyReplayOptions;
13
+ shell: ShellInfo;
14
+ }
15
+ /**
16
+ * A recorder handle that exists before the recorder does.
17
+ *
18
+ * A shell learns its app id and version asynchronously (an IPC round trip in
19
+ * Electron, a Tauri command), but `init` has to hand something back at once so
20
+ * an app can call `track` on its next line. Calls made in between are queued
21
+ * in order and replayed on the real handle; `flush` waits for it.
22
+ */
23
+ interface DeferredHandle extends RecorderHandle {
24
+ /** Resolves once the recorder has started (or failed to). Never rejects. */
25
+ ready: Promise<void>;
26
+ }
27
+
28
+ declare const SDK_NAME = "@anyreplay/electron";
29
+ declare const SDK_VERSION: string;
30
+
31
+ interface AnyReplayElectronOptions extends AnyReplayOptions {
32
+ /**
33
+ * The app id. Normally left out: the main process supplies it
34
+ * (`setupAnyReplay({ appId })`). Set here, it wins.
35
+ */
36
+ appId?: string;
37
+ /** The app version. Normally left out: the main process supplies `app.getVersion()`. */
38
+ appVersion?: string;
39
+ /**
40
+ * Send what has been recorded whenever the window loses focus. On by
41
+ * default: a desktop app is rarely closed the way a tab is, and the main
42
+ * process can be killed without `before-quit`.
43
+ */
44
+ flushOnBlur?: boolean;
45
+ }
46
+ type AnyReplayElectron = DeferredHandle;
47
+ /**
48
+ * Starts recording this window. Call it once in the renderer, as early as you
49
+ * can:
50
+ *
51
+ * ```ts
52
+ * import { init } from '@anyreplay/electron';
53
+ * const replay = init({ projectKey: 'ar_pk_live_…' });
54
+ * replay.track('document_opened');
55
+ * ```
56
+ *
57
+ * The app id and version come from the main process through the preload
58
+ * bridge (`exposeAnyReplay()` and `setupAnyReplay()`), so recording starts a
59
+ * moment after this returns; calls made in between are queued. A second call
60
+ * returns the first handle.
61
+ *
62
+ * Each window is its own session: the session id lives in the window's
63
+ * `sessionStorage`, the visitor id in `localStorage`, which Electron keeps
64
+ * across launches and shares between windows of the same session partition.
65
+ */
66
+ declare function init(options: AnyReplayElectronOptions): AnyReplayElectron;
67
+ /**
68
+ * The browser SDK's options for this window — the installer's, with the app
69
+ * id filled in — and the shell it runs in, which only the wrapper knows.
70
+ */
71
+ declare function electronSetup(options: AnyReplayElectronOptions, info: AppInfo): RecorderSetup;
72
+ /**
73
+ * A main-process error as an `Error` again — it crossed IPC as plain fields —
74
+ * for the browser SDK to redact and clip like any other.
75
+ */
76
+ declare function mainError(error: MainErrorMessage | undefined): Error;
77
+ /** For tests: forget the handle `init` returned. */
78
+ declare function __resetForTests(): void;
79
+
80
+ export { type AnyReplayElectron, type AnyReplayElectronOptions, AppInfo, MainErrorMessage, type RecorderSetup, SDK_NAME, SDK_VERSION, __resetForTests, electronSetup, init, mainError };
@@ -0,0 +1,80 @@
1
+ import { RecorderHandle, AnyReplayOptions, ShellInfo } from '@anyreplay/browser';
2
+ import { A as AppInfo, M as MainErrorMessage } from './bridge-BQKD7rrG.js';
3
+ export { a as AnyReplayBridge, B as BRIDGE_KEY, C as CHANNELS } from './bridge-BQKD7rrG.js';
4
+
5
+ /**
6
+ * What a desktop shell hands the browser SDK: its options, and who is
7
+ * recording (docs/SDK-CONTRACT.md §1, §4) — the platform and SDK the session
8
+ * reports and the app's version. `createRecorder(options, shell)` is how a
9
+ * wrapper names itself without a public option a website could set.
10
+ */
11
+ interface RecorderSetup {
12
+ options: AnyReplayOptions;
13
+ shell: ShellInfo;
14
+ }
15
+ /**
16
+ * A recorder handle that exists before the recorder does.
17
+ *
18
+ * A shell learns its app id and version asynchronously (an IPC round trip in
19
+ * Electron, a Tauri command), but `init` has to hand something back at once so
20
+ * an app can call `track` on its next line. Calls made in between are queued
21
+ * in order and replayed on the real handle; `flush` waits for it.
22
+ */
23
+ interface DeferredHandle extends RecorderHandle {
24
+ /** Resolves once the recorder has started (or failed to). Never rejects. */
25
+ ready: Promise<void>;
26
+ }
27
+
28
+ declare const SDK_NAME = "@anyreplay/electron";
29
+ declare const SDK_VERSION: string;
30
+
31
+ interface AnyReplayElectronOptions extends AnyReplayOptions {
32
+ /**
33
+ * The app id. Normally left out: the main process supplies it
34
+ * (`setupAnyReplay({ appId })`). Set here, it wins.
35
+ */
36
+ appId?: string;
37
+ /** The app version. Normally left out: the main process supplies `app.getVersion()`. */
38
+ appVersion?: string;
39
+ /**
40
+ * Send what has been recorded whenever the window loses focus. On by
41
+ * default: a desktop app is rarely closed the way a tab is, and the main
42
+ * process can be killed without `before-quit`.
43
+ */
44
+ flushOnBlur?: boolean;
45
+ }
46
+ type AnyReplayElectron = DeferredHandle;
47
+ /**
48
+ * Starts recording this window. Call it once in the renderer, as early as you
49
+ * can:
50
+ *
51
+ * ```ts
52
+ * import { init } from '@anyreplay/electron';
53
+ * const replay = init({ projectKey: 'ar_pk_live_…' });
54
+ * replay.track('document_opened');
55
+ * ```
56
+ *
57
+ * The app id and version come from the main process through the preload
58
+ * bridge (`exposeAnyReplay()` and `setupAnyReplay()`), so recording starts a
59
+ * moment after this returns; calls made in between are queued. A second call
60
+ * returns the first handle.
61
+ *
62
+ * Each window is its own session: the session id lives in the window's
63
+ * `sessionStorage`, the visitor id in `localStorage`, which Electron keeps
64
+ * across launches and shares between windows of the same session partition.
65
+ */
66
+ declare function init(options: AnyReplayElectronOptions): AnyReplayElectron;
67
+ /**
68
+ * The browser SDK's options for this window — the installer's, with the app
69
+ * id filled in — and the shell it runs in, which only the wrapper knows.
70
+ */
71
+ declare function electronSetup(options: AnyReplayElectronOptions, info: AppInfo): RecorderSetup;
72
+ /**
73
+ * A main-process error as an `Error` again — it crossed IPC as plain fields —
74
+ * for the browser SDK to redact and clip like any other.
75
+ */
76
+ declare function mainError(error: MainErrorMessage | undefined): Error;
77
+ /** For tests: forget the handle `init` returned. */
78
+ declare function __resetForTests(): void;
79
+
80
+ export { type AnyReplayElectron, type AnyReplayElectronOptions, AppInfo, MainErrorMessage, type RecorderSetup, SDK_NAME, SDK_VERSION, __resetForTests, electronSetup, init, mainError };