@vitrinka/web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +93 -0
  3. package/README.md +177 -0
  4. package/build/index.d.ts +11 -0
  5. package/build/index.js +1 -0
  6. package/build/next.d.ts +23 -0
  7. package/build/next.js +27 -0
  8. package/build/protocol/index.d.ts +74 -0
  9. package/build/protocol/index.js +22 -0
  10. package/build/recorder/RecorderProvider.d.ts +17 -0
  11. package/build/recorder/RecorderProvider.js +126 -0
  12. package/build/recorder/api-status.d.ts +6 -0
  13. package/build/recorder/api-status.js +6 -0
  14. package/build/recorder/api.d.ts +40 -0
  15. package/build/recorder/api.js +84 -0
  16. package/build/recorder/capture/click.d.ts +17 -0
  17. package/build/recorder/capture/click.js +77 -0
  18. package/build/recorder/capture/console.d.ts +3 -0
  19. package/build/recorder/capture/console.js +89 -0
  20. package/build/recorder/capture/nav.d.ts +8 -0
  21. package/build/recorder/capture/nav.js +54 -0
  22. package/build/recorder/capture/net.d.ts +22 -0
  23. package/build/recorder/capture/net.js +506 -0
  24. package/build/recorder/capture/redact.d.ts +38 -0
  25. package/build/recorder/capture/redact.js +54 -0
  26. package/build/recorder/capture/rrweb.d.ts +10 -0
  27. package/build/recorder/capture/rrweb.js +76 -0
  28. package/build/recorder/config.d.ts +50 -0
  29. package/build/recorder/config.js +100 -0
  30. package/build/recorder/control.d.ts +29 -0
  31. package/build/recorder/control.js +63 -0
  32. package/build/recorder/hud/AnnotateOverlay.d.ts +25 -0
  33. package/build/recorder/hud/AnnotateOverlay.js +122 -0
  34. package/build/recorder/hud/Hud.d.ts +12 -0
  35. package/build/recorder/hud/Hud.js +190 -0
  36. package/build/recorder/hud/LinkSheet.d.ts +26 -0
  37. package/build/recorder/hud/LinkSheet.js +15 -0
  38. package/build/recorder/hud/RecorderPill.d.ts +36 -0
  39. package/build/recorder/hud/RecorderPill.js +73 -0
  40. package/build/recorder/hud/Sheet.d.ts +20 -0
  41. package/build/recorder/hud/Sheet.js +36 -0
  42. package/build/recorder/hud/host.d.ts +27 -0
  43. package/build/recorder/hud/host.js +170 -0
  44. package/build/recorder/hud/icons.d.ts +15 -0
  45. package/build/recorder/hud/icons.js +40 -0
  46. package/build/recorder/hud/styles.d.ts +13 -0
  47. package/build/recorder/hud/styles.js +111 -0
  48. package/build/recorder/index.d.ts +46 -0
  49. package/build/recorder/index.js +61 -0
  50. package/build/recorder/link.d.ts +18 -0
  51. package/build/recorder/link.js +37 -0
  52. package/build/recorder/queue.d.ts +163 -0
  53. package/build/recorder/queue.js +642 -0
  54. package/build/recorder/session.d.ts +73 -0
  55. package/build/recorder/session.js +246 -0
  56. package/build/recorder/state.d.ts +26 -0
  57. package/build/recorder/state.js +42 -0
  58. package/build/recorder/storage/index.d.ts +35 -0
  59. package/build/recorder/storage/index.js +69 -0
  60. package/build/recorder/storage/memory.d.ts +2 -0
  61. package/build/recorder/storage/memory.js +2 -0
  62. package/package.json +77 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @vitrinka/web
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release: the React DOM journey recorder (`@vitrinka/web/recorder`)
6
+ — rrweb DOM stream (chunked like the browser extension), DOM clicks,
7
+ navigation, `console.error` / uncaught errors, fetch/XHR with
8
+ `@vitrinka/redact`; the pill HUD in a top-layer shadow host with notes,
9
+ element/region annotations and keyboard shortcuts; `window.__vitrinkaRecorder`
10
+ control handle; `withVitrinkaRecorder` (`@vitrinka/web/next`) build guard.
11
+ - Device link: no key needed — the pill links the device (`@vitrinka/link`),
12
+ stores the minted token under `vitrinka.recorder.link`; `recorderKey` is
13
+ for CI. The runtime strip is the URL alone.
package/LICENSE ADDED
@@ -0,0 +1,93 @@
1
+ Elastic License 2.0
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject to
14
+ the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set of
20
+ the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other notices
27
+ of the licensor in the software. Any use of the licensor’s trademarks is subject
28
+ to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, sell, offer for
34
+ sale, import and have imported the software, in each case subject to the
35
+ limitations and conditions in this license. This license does not cover any
36
+ patent claims that you cause to be infringed by modifications or additions to
37
+ the software. If you or your company make any written claim that the software
38
+ infringes or contributes to infringement of any patent, your patent license for
39
+ the software granted under these terms ends immediately. If your company makes
40
+ such a claim, your patent license ends immediately for work on behalf of your
41
+ company.
42
+
43
+ ## Notices
44
+
45
+ You must ensure that anyone who gets a copy of any part of the software from you
46
+ also gets a copy of these terms.
47
+
48
+ If you modify the software, you must include in any modified copies of the
49
+ software prominent notices stating that you have modified the software.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not imply any licenses other than those expressly granted in
54
+ these terms.
55
+
56
+ ## Termination
57
+
58
+ If you use the software in violation of these terms, such use is not licensed,
59
+ and your licenses will automatically terminate. If the licensor provides you
60
+ with a notice of your violation, and you cease all violation of this license no
61
+ later than 30 days after you receive that notice, your licenses will be
62
+ reinstated retroactively. However, if you violate these terms after such
63
+ reinstatement, any additional violation of these terms will cause your licenses
64
+ to terminate automatically and permanently.
65
+
66
+ ## No Liability
67
+
68
+ *As far as the law allows, the software comes as is, without any warranty or
69
+ condition, and the licensor will not be liable to you for any damages arising
70
+ out of these terms or the use or nature of the software, under any kind of
71
+ legal claim.*
72
+
73
+ ## Definitions
74
+
75
+ The **licensor** is the entity offering these terms, and the **software** is the
76
+ software the licensor makes available under these terms, including any portion
77
+ of it.
78
+
79
+ **you** refers to the individual or entity agreeing to these terms.
80
+
81
+ **your company** is any legal entity, sole proprietorship, or other kind of
82
+ organization that you work for, plus all organizations that have control over,
83
+ are under the control of, or are under common control with that organization.
84
+ **control** means ownership of substantially all the assets of an entity, or the
85
+ power to direct its management and policies by vote, contract, or otherwise.
86
+ Control can be direct or indirect.
87
+
88
+ **your licenses** are all the licenses granted to you for the software under
89
+ these terms.
90
+
91
+ **use** means anything you do with the software requiring one of your licenses.
92
+
93
+ **trademark** means trademarks, service marks, and similar rights.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # @vitrinka/web
2
+
3
+ The vitrinka toolkit for React DOM apps — today, the **journey recorder**:
4
+ record a manual-testing session straight from the app under test into a
5
+ vitrinka board. The web sibling of [`@vitrinka/expo`](../expo) and of the
6
+ browser extension: same wire protocol, same redaction engine, no extension
7
+ to install.
8
+
9
+ What a session carries: the **rrweb DOM stream** (the keyframe — no
10
+ screenshots), **clicks** (selector, text, rect), **navigation**, **network**
11
+ (fetch + XHR, headers + bodies redacted), **console errors**, and the
12
+ **notes and annotations** you type in the pill. Everything is redacted by
13
+ [`@vitrinka/redact`](../redact) before it is buffered; the full contract is
14
+ [`docs/PROTOCOL.md`](../../docs/PROTOCOL.md).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ bun add @vitrinka/web rrweb
20
+ # or: npm i @vitrinka/web rrweb
21
+ ```
22
+
23
+ Peer deps: `react` ≥ 18, `react-dom` ≥ 18, `rrweb` ^2.
24
+
25
+ ## Mount (Next.js app router)
26
+
27
+ ```tsx
28
+ // app/layout.tsx
29
+ import { VitrinkaRecorderRoot, VitrinkaRecorderPill } from '@vitrinka/web/recorder';
30
+
31
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
32
+ return (
33
+ <html lang="en">
34
+ <body>
35
+ <VitrinkaRecorderRoot
36
+ url={process.env.NEXT_PUBLIC_VITRINKA_URL}
37
+ recorderKey={process.env.NEXT_PUBLIC_VITRINKA_KEY}
38
+ appVersion={process.env.NEXT_PUBLIC_APP_VERSION}
39
+ >
40
+ {children}
41
+ <VitrinkaRecorderPill />
42
+ </VitrinkaRecorderRoot>
43
+ </body>
44
+ </html>
45
+ );
46
+ }
47
+ ```
48
+
49
+ `VitrinkaRecorderRoot` installs the capture lanes; `VitrinkaRecorderPill`
50
+ renders the HUD into its own shadow host on `<html>` (top layer, never inside
51
+ your DOM). **When `url` is empty the root renders its children and starts
52
+ nothing** — a build without the URL carries an inert recorder.
53
+
54
+ **No key is needed.** Dev/preview builds set only `NEXT_PUBLIC_VITRINKA_URL`;
55
+ testers **link their device from the pill**: the pill shows **Link
56
+ recorder**, the sheet shows a short code, **Open vitrinka** (approve on this
57
+ device) and a QR (scan from your phone); once approved the server mints an
58
+ ingest-only `vkr_` token, stored in `localStorage` under
59
+ `vitrinka.recorder.link`, and recording starts. **Unlink** in the ⋯ menu
60
+ forgets it — so does a 401 from the server. `recorderKey` (an admin-minted
61
+ `vkr_` key) is for CI, e2e and unattended builds only; when passed it wins
62
+ over the link. The prop is `recorderKey`, not `key`: React reserves `key`
63
+ and never delivers it to a component.
64
+
65
+ Navigation is observed through `history.pushState` / `replaceState` /
66
+ `popstate`, which covers Next, React Router and friends. A router that
67
+ navigates without History can feed its pathname instead:
68
+
69
+ ```tsx
70
+ import { usePathname } from 'next/navigation';
71
+ import { useRecorderRoute } from '@vitrinka/web/recorder';
72
+
73
+ function RouteFeed() {
74
+ useRecorderRoute(usePathname());
75
+ return null;
76
+ }
77
+ ```
78
+
79
+ Vite and plain React work the same way — pass `url` / `recorderKey` from
80
+ `import.meta.env` (the `NEXT_PUBLIC_*` fallback only applies where a
81
+ bundler inlines `process.env`).
82
+
83
+ ### Env vars
84
+
85
+ | Var | What |
86
+ |---|---|
87
+ | `NEXT_PUBLIC_VITRINKA_URL` | Your vitrinka server, e.g. `https://app.vitrinka.ai` |
88
+ | `NEXT_PUBLIC_VITRINKA_KEY` | Optional — CI / unattended only. A **`vkr_` recorder key** minted in vitrinka under Settings → project → Recorder keys: project-pinned, origin-allowlisted, valid only on the session routes — never a `vkp_`/`vks_` API key. Testers link instead. |
89
+ | `VITRINKA_RECORDER_LANE` | The build's lane for the guard below (`development`, `preview`, …) |
90
+
91
+ ### The build guard
92
+
93
+ A production build must never ship the recorder by accident. Wrap your Next
94
+ config:
95
+
96
+ ```js
97
+ // next.config.js
98
+ const { withVitrinkaRecorder } = require('@vitrinka/web/next');
99
+ module.exports = withVitrinkaRecorder({ /* your config */ });
100
+ ```
101
+
102
+ `next build` (`NODE_ENV=production`) then **refuses** when a baked
103
+ `NEXT_PUBLIC_VITRINKA_KEY` is set and `VITRINKA_RECORDER_LANE` is not one of
104
+ the allowed lanes — the URL alone is allowed everywhere (dev/preview builds
105
+ set only the URL; testers link from the pill; a key is for CI) (default `development`, `preview`; override with
106
+ `allowedLanes`, and the var names with `laneVar` / `keyVar`). Unset the key
107
+ for the production lane, or set the lane on the preview one. The config is
108
+ returned unchanged otherwise.
109
+
110
+ ## Using the pill
111
+
112
+ Unlinked: **Link recorder** bottom-right. Linked and idle: a quiet dot — click to **start** (the session title is
113
+ `document.title`, or the `title` prop). Recording: rec dot · timer · name ·
114
+ sync glyph · controls (keycaps show on hover):
115
+
116
+ | Control | Shortcut | What |
117
+ |---|---|---|
118
+ | ⏸ Pause / ▶ Resume | ⌥⇧P (Alt⇧P) | freezes the clock and capture |
119
+ | ✎ Note | ⌥⇧N | the 360px sheet — Enter sends, ⇧Enter newline, Esc / ✕ / click-outside cancel (the draft survives a cancel) |
120
+ | ⌖ Annotate | ⌥⇧A | click an element or drag a region, then describe it; `board` (an annotation on the board) or `task` (also filed as an intake draft) |
121
+ | ⋯ | | **Open board** (the server-minted link) · **Stop recording** · **Unlink** |
122
+
123
+ The sync glyph is honest: ✓ means the server confirmed it holds everything
124
+ captured; a second line unfolds only for a backlog, an outage (`offline ·
125
+ N held · retrying` — nothing is dropped, the tail is kept in `localStorage`)
126
+ or a session the server closed. Stop drains first and refuses while the
127
+ server is unreachable — stop again once online.
128
+
129
+ The sheet renders inside the topmost open dialog when one exists, so a
130
+ Radix focus trap or a `<dialog>.showModal()` never fights it, and nothing
131
+ you do on the pill reaches the page (a "close on outside click" never fires
132
+ because of the recorder).
133
+
134
+ ## Driving it from code
135
+
136
+ `window.__vitrinkaRecorder` — for agents and tests:
137
+
138
+ ```ts
139
+ await __vitrinkaRecorder.start({ title: 'checkout', tags: ['ai'] });
140
+ __vitrinkaRecorder.note('price flashes on hover');
141
+ const { boardUrl } = await __vitrinkaRecorder.stop();
142
+ __vitrinkaRecorder.status(); // { recording, sessionId, elapsedMs, queued, synced, … }
143
+ ```
144
+
145
+ ## What is captured, exactly
146
+
147
+ | Lane | Event `kind` | Payload |
148
+ |---|---|---|
149
+ | rrweb | `rrweb` | `{count}` + `blobKey` — the batch itself is uploaded as a chunk; inputs always masked, all text under a `maskAllText` policy |
150
+ | clicks | `click` | `{selector, text, rect, route}` |
151
+ | navigation | `nav` | `{url, route, spa}` |
152
+ | network | `net` | `{method, url, status, ms, reqHeaders, resHeaders, reqBody, resBody, via}` — redacted, capped at 64 KiB per body; the recorder's own uploads are never recorded |
153
+ | console | `console` | `{level: 'error', text}` |
154
+ | notes | `note` | `{text, route}`; annotations add `{rect, selector, annotate: true, task?}` |
155
+
156
+ Data goes to your vitrinka server only. Details, the redaction rules and the
157
+ policy fetch: [`docs/PROTOCOL.md`](../../docs/PROTOCOL.md).
158
+
159
+ ## Storage
160
+
161
+ The undelivered tail lives in `localStorage` (a recording survives a reload
162
+ of its tab). Where that is unavailable (private mode, a storage-disabled
163
+ profile) the recorder falls back to memory and says so once; plug your own
164
+ synchronous driver with `configureRecorderStorage()` before mounting.
165
+
166
+ ## Development
167
+
168
+ ```bash
169
+ bun run --filter '@vitrinka/redact' build # the workspace dep resolves through build/
170
+ bun run --filter '@vitrinka/web' typecheck
171
+ bun run --filter '@vitrinka/web' test # bun test src
172
+ bun run test:e2e:web # one headless Chromium spec (packages/web/e2e)
173
+ ```
174
+
175
+ A release is a tag: bump `version` here (and `RECORDER_VERSION` in
176
+ `src/recorder/session.ts`), merge, then `git tag web-vX.Y.Z && git push
177
+ origin web-vX.Y.Z`.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @vitrinka/web — the vitrinka toolkit for React DOM apps.
3
+ *
4
+ * The root export carries only the wire protocol types; the tools live on
5
+ * subpaths so an app bundles exactly what it imports:
6
+ *
7
+ * @vitrinka/web/recorder journey recorder (VitrinkaRecorderRoot, VitrinkaRecorderPill)
8
+ * @vitrinka/web/next withVitrinkaRecorder (next.config.js build guard, Node-only)
9
+ * @vitrinka/web/protocol recorder↔server wire types
10
+ */
11
+ export type * from './protocol';
package/build/index.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `withVitrinkaRecorder` — the build guard for Next apps (`next.config.js`).
3
+ * Node-only, no React import.
4
+ *
5
+ * The recorder ships in any build whose env carries the key; this guard is
6
+ * the safety boundary: a PRODUCTION build (`NODE_ENV=production`) that has
7
+ * the key var set must also declare an allowed lane in `VITRINKA_RECORDER_LANE`
8
+ * (default: `development` or `preview`), or the build refuses with a clear
9
+ * error. The URL alone is allowed everywhere (testers link from the pill); the
10
+ * config is returned unchanged otherwise.
11
+ */
12
+ export interface WithVitrinkaRecorderOptions {
13
+ /** Lanes a production build may carry the recorder key in. */
14
+ allowedLanes?: readonly string[];
15
+ /** Env var naming the build's lane. */
16
+ laneVar?: string;
17
+ /** Env var carrying the recorder key. */
18
+ keyVar?: string;
19
+ /** Env var carrying the vitrinka URL (informational — only the key gates). */
20
+ urlVar?: string;
21
+ }
22
+ export declare const DEFAULT_ALLOWED_LANES: readonly string[];
23
+ export declare function withVitrinkaRecorder<T>(nextConfig: T, opts?: WithVitrinkaRecorderOptions): T;
package/build/next.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `withVitrinkaRecorder` — the build guard for Next apps (`next.config.js`).
3
+ * Node-only, no React import.
4
+ *
5
+ * The recorder ships in any build whose env carries the key; this guard is
6
+ * the safety boundary: a PRODUCTION build (`NODE_ENV=production`) that has
7
+ * the key var set must also declare an allowed lane in `VITRINKA_RECORDER_LANE`
8
+ * (default: `development` or `preview`), or the build refuses with a clear
9
+ * error. The URL alone is allowed everywhere (testers link from the pill); the
10
+ * config is returned unchanged otherwise.
11
+ */
12
+ export const DEFAULT_ALLOWED_LANES = ['development', 'preview'];
13
+ export function withVitrinkaRecorder(nextConfig, opts = {}) {
14
+ const allowed = opts.allowedLanes ?? DEFAULT_ALLOWED_LANES;
15
+ const laneVar = opts.laneVar ?? 'VITRINKA_RECORDER_LANE';
16
+ const keyVar = opts.keyVar ?? 'NEXT_PUBLIC_VITRINKA_KEY';
17
+ const env = process.env;
18
+ if (env.NODE_ENV === 'production' && env[keyVar]) {
19
+ const lane = env[laneVar] ?? '';
20
+ if (!allowed.includes(lane)) {
21
+ throw new Error(`vitrinka: ${keyVar} is set on a production build but ${laneVar}=${JSON.stringify(lane)} ` +
22
+ `is not an allowed recorder lane (${allowed.join(', ')}). ` +
23
+ `Unset ${keyVar} for this build, or set ${laneVar} to one of the allowed lanes.`);
24
+ }
25
+ }
26
+ return nextConfig;
27
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * MIRROR of `packages/expo/src/protocol/index.ts` — platform-first packages
3
+ * each own their copy, and this one MUST stay byte-compatible with expo's
4
+ * (same shapes, same field names): the vitrinka server pins ONE ingest
5
+ * contract and both recorders ride it. Edit expo's first, then re-mirror.
6
+ */
7
+ /**
8
+ * The recorder↔server wire contract — `@vitrinka/web/protocol` (mirrors `@vitrinka/expo/protocol`).
9
+ *
10
+ * Everything the recorders send rides these shapes over four routes:
11
+ *
12
+ * POST /api/v1/sessions SessionCreateRequest → SessionCreateResponse
13
+ * POST /api/v1/sessions/:id/events { events: RecorderEvent[] }
14
+ * POST /api/v1/sessions/:id/shot?seq=N (image body; the event stream
15
+ * carries a matching 'shot' event)
16
+ * PATCH /api/v1/sessions/:id { status: SessionStatus }
17
+ *
18
+ * This module is types-only and dependency-free on purpose: the vitrinka
19
+ * server pins its ingest contract against it, so a change here is a change to
20
+ * the wire — version it deliberately.
21
+ */
22
+ /** One captured event in a session's ordered stream. */
23
+ export interface RecorderEvent {
24
+ /** Recorder-allocated, strictly increasing; the delivery-ack unit. */
25
+ seq: number;
26
+ /** ISO timestamp at capture. */
27
+ ts: string;
28
+ /** Timeline lane (tab/section grouping). */
29
+ tabId: string;
30
+ /** Full pathname/host context the event happened on. */
31
+ tabHost: string;
32
+ /** Event kind: 'nav' | 'click' | 'shot' | 'note' | 'net' | 'console' | … */
33
+ kind: string;
34
+ payload?: Record<string, unknown>;
35
+ /** Server blob reference for 'shot' events (set after upload). */
36
+ blobKey?: string;
37
+ }
38
+ export interface SessionCreateRequest {
39
+ /** App id the server resolves project+environment from. */
40
+ app: string;
41
+ title: string;
42
+ /** Explicit server lane; omitted = the server's app-id rule decides. */
43
+ environment?: string;
44
+ meta?: {
45
+ /** Recorder implementation + protocol revision, e.g. 'vitrinka-expo/1'. */
46
+ recorder?: string;
47
+ platform?: string;
48
+ appVersion?: string;
49
+ /** 'ai' marks a machine-driven run. */
50
+ driver?: string;
51
+ [key: string]: unknown;
52
+ };
53
+ }
54
+ export interface SessionCreateResponse {
55
+ id: string;
56
+ project: string;
57
+ environment: string;
58
+ title: string;
59
+ }
60
+ export type SessionStatus = 'recording' | 'paused' | 'done';
61
+ /** GET /api/v1/sessions/:id — the reconcile poll's answer. */
62
+ export interface SessionReconcileResponse {
63
+ /** Highest seq the server actually holds. */
64
+ maxSeq?: number;
65
+ status?: string;
66
+ deletedAt?: string | null;
67
+ }
68
+ /** PATCH response when a session completes (board projection). */
69
+ export interface SessionDone {
70
+ boardSlug?: string;
71
+ board?: {
72
+ url?: string;
73
+ };
74
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MIRROR of `packages/expo/src/protocol/index.ts` — platform-first packages
3
+ * each own their copy, and this one MUST stay byte-compatible with expo's
4
+ * (same shapes, same field names): the vitrinka server pins ONE ingest
5
+ * contract and both recorders ride it. Edit expo's first, then re-mirror.
6
+ */
7
+ /**
8
+ * The recorder↔server wire contract — `@vitrinka/web/protocol` (mirrors `@vitrinka/expo/protocol`).
9
+ *
10
+ * Everything the recorders send rides these shapes over four routes:
11
+ *
12
+ * POST /api/v1/sessions SessionCreateRequest → SessionCreateResponse
13
+ * POST /api/v1/sessions/:id/events { events: RecorderEvent[] }
14
+ * POST /api/v1/sessions/:id/shot?seq=N (image body; the event stream
15
+ * carries a matching 'shot' event)
16
+ * PATCH /api/v1/sessions/:id { status: SessionStatus }
17
+ *
18
+ * This module is types-only and dependency-free on purpose: the vitrinka
19
+ * server pins its ingest contract against it, so a change here is a change to
20
+ * the wire — version it deliberately.
21
+ */
22
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Recorder root — mounted ONLY when `url` + `key` are present (see index.ts).
3
+ * Installs the capture lanes once per document, recovers a session that
4
+ * survived a reload, and keeps the rrweb lane in step with the session.
5
+ */
6
+ import { type ReactElement, type ReactNode } from 'react';
7
+ import { type RecorderConfig } from './config';
8
+ export declare function RecorderProvider({ config, children, }: {
9
+ config: RecorderConfig;
10
+ children?: ReactNode;
11
+ }): ReactElement;
12
+ /**
13
+ * Feed the recorder a router's pathname (Next: `usePathname()`). The default
14
+ * History wrap already sees `pushState`; this hook exists for routers that
15
+ * navigate without it and is idempotent alongside it.
16
+ */
17
+ export declare function useRecorderRoute(pathname: string | null | undefined): void;
@@ -0,0 +1,126 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Recorder root — mounted ONLY when `url` + `key` are present (see index.ts).
4
+ * Installs the capture lanes once per document, recovers a session that
5
+ * survived a reload, and keeps the rrweb lane in step with the session.
6
+ */
7
+ import { useEffect } from 'react';
8
+ import { installClickLane } from './capture/click';
9
+ import { patchConsole, unpatchConsole } from './capture/console';
10
+ import { installNavLane, noteNavigation, primeNavigation } from './capture/nav';
11
+ import { patchNetwork, unpatchNetwork } from './capture/net';
12
+ import { checkoutRRWeb, flushRRWeb, startRRWeb, stopRRWeb } from './capture/rrweb';
13
+ import { configureRecorder } from './config';
14
+ import { installControl } from './control';
15
+ import { installUnauthorizedHandler } from './link';
16
+ import { insideHud } from './hud/host';
17
+ import { armReconcile, flush, getState, persistNow, reconcile, scheduleFlush } from './queue';
18
+ import { onBeforeStop, recoverRedactionPolicy } from './session';
19
+ import { annotateState, setTabIdentity, subscribe } from './state';
20
+ const TAB_KEY = 'vitrinka.tab';
21
+ /** One lane per browser tab: a per-tab id kept in sessionStorage (survives reloads). */
22
+ function tabId() {
23
+ try {
24
+ const ss = globalThis.sessionStorage;
25
+ const have = ss.getItem(TAB_KEY);
26
+ if (have)
27
+ return have;
28
+ const id = Math.random().toString(36).slice(2, 10);
29
+ ss.setItem(TAB_KEY, id);
30
+ return id;
31
+ }
32
+ catch {
33
+ return 'root';
34
+ }
35
+ }
36
+ export function RecorderProvider({ config, children, }) {
37
+ configureRecorder(config);
38
+ useEffect(() => {
39
+ configureRecorder(config);
40
+ }, [config]);
41
+ useEffect(() => {
42
+ setTabIdentity(tabId(), location.host);
43
+ primeNavigation();
44
+ // The fetch/XHR and console patches are idempotent (a globalThis mark)
45
+ // and UNINSTALLED on unmount below, so the recorder never outlives its
46
+ // tree; the History wrap is idempotent too and stays (a nav event with no
47
+ // session is dropped at the queue, so it costs nothing).
48
+ patchNetwork();
49
+ patchConsole();
50
+ installNavLane();
51
+ const uninstallClicks = installClickLane({
52
+ ignore: (t) => annotateState.active || insideHud(t),
53
+ });
54
+ const uninstallControl = installControl();
55
+ const uninstall401 = installUnauthorizedHandler();
56
+ // Keep the rrweb lane in step with the session: start on record, a fresh
57
+ // checkout on resume, ship-and-stop on stop.
58
+ let wasRecording = false;
59
+ let wasPaused = false;
60
+ const syncLanes = () => {
61
+ const rec = getState();
62
+ const recording = rec !== null && !rec.dead;
63
+ if (recording && !wasRecording)
64
+ startRRWeb();
65
+ else if (!recording && wasRecording)
66
+ stopRRWeb();
67
+ else if (recording && wasPaused && !rec.paused)
68
+ checkoutRRWeb();
69
+ wasRecording = recording;
70
+ wasPaused = rec?.paused ?? false;
71
+ };
72
+ const unsubscribe = subscribe(syncLanes);
73
+ const offBeforeStop = onBeforeStop(flushRRWeb);
74
+ // A reload mid-session: the durable tail needs a drain, the reconcile
75
+ // poll re-arming, the policy re-applying, and the new document is a nav.
76
+ scheduleFlush();
77
+ if (getState()) {
78
+ recoverRedactionPolicy();
79
+ armReconcile();
80
+ noteNavigation();
81
+ }
82
+ syncLanes();
83
+ // Leaving the document: ship the rrweb tail, persist, and try a keepalive
84
+ // flush of a small events batch so the last steps ride out.
85
+ const onPageHide = () => {
86
+ flushRRWeb();
87
+ persistNow();
88
+ void flush({ keepalive: true });
89
+ };
90
+ const onVisible = () => {
91
+ if (document.visibilityState === 'visible' && getState()) {
92
+ void flush();
93
+ void reconcile();
94
+ }
95
+ };
96
+ addEventListener('pagehide', onPageHide);
97
+ document.addEventListener('visibilitychange', onVisible);
98
+ return () => {
99
+ removeEventListener('pagehide', onPageHide);
100
+ document.removeEventListener('visibilitychange', onVisible);
101
+ unsubscribe();
102
+ offBeforeStop();
103
+ uninstallClicks();
104
+ uninstallControl();
105
+ uninstall401();
106
+ stopRRWeb();
107
+ unpatchNetwork();
108
+ unpatchConsole();
109
+ };
110
+ // The lanes install once per mount; config changes are handled above.
111
+ // eslint-disable-next-line react-hooks/exhaustive-deps
112
+ }, []);
113
+ return _jsx(_Fragment, { children: children });
114
+ }
115
+ /**
116
+ * Feed the recorder a router's pathname (Next: `usePathname()`). The default
117
+ * History wrap already sees `pushState`; this hook exists for routers that
118
+ * navigate without it and is idempotent alongside it.
119
+ */
120
+ export function useRecorderRoute(pathname) {
121
+ useEffect(() => {
122
+ if (pathname == null)
123
+ return;
124
+ noteNavigation();
125
+ }, [pathname]);
126
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Transport-status vocabulary — ONE copy lives in `@vitrinka/link` (the
3
+ * zero-dependency package every recorder already depends on); this module
4
+ * keeps the import path stable for `api.ts` and the tests.
5
+ */
6
+ export { permanentStatus, VitrinkaApiError } from '@vitrinka/link';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Transport-status vocabulary — ONE copy lives in `@vitrinka/link` (the
3
+ * zero-dependency package every recorder already depends on); this module
4
+ * keeps the import path stable for `api.ts` and the tests.
5
+ */
6
+ export { permanentStatus, VitrinkaApiError } from '@vitrinka/link';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Vitrinka API client for the web journey recorder.
3
+ *
4
+ * Ingest contract, shared with the Expo recorder and the browser extension:
5
+ * GET /api/v1/recorder/policy workspace redaction policy
6
+ * POST /api/v1/sessions {host, title, environment?, meta} → session
7
+ * POST /api/v1/sessions/:id/events {events: [...]}
8
+ * POST /api/v1/sessions/:id/chunk?seq=N (rrweb batch body, application/json)
9
+ * POST /api/v1/sessions/:id/tags {tags} (non-fatal)
10
+ * GET /api/v1/sessions/:id reconcile
11
+ * PATCH /api/v1/sessions/:id {status: recording|paused|done}
12
+ *
13
+ * Every call: `authorization: Bearer <key>`, `credentials: "omit"` (the key
14
+ * IS the credential; cookies never ride), `mode: "cors"`.
15
+ */
16
+ import type { RedactionPolicy } from '@vitrinka/redact';
17
+ export { permanentStatus, VitrinkaApiError } from './api-status';
18
+ export declare function onUnauthorized(fn: () => void): () => void;
19
+ /**
20
+ * Fetch the workspace redaction policy at session start. NEVER rejects: null
21
+ * (server too old, network down, 4xx) means the engine's safe defaults — fail
22
+ * closed, never capture-everything.
23
+ */
24
+ export declare function fetchPolicy(): Promise<RedactionPolicy | null>;
25
+ export interface ApiOptions {
26
+ /**
27
+ * Survive page unload (the pagehide flush). Browsers cap keepalive bodies at
28
+ * 64 KiB — callers pass it only for a small tail batch.
29
+ */
30
+ keepalive?: boolean;
31
+ }
32
+ export declare function api<T = Record<string, unknown>>(method: string, path: string, body?: unknown, opts?: ApiOptions): Promise<T>;
33
+ /**
34
+ * Upload one rrweb chunk (an already-serialized JSON array of rrweb events)
35
+ * under a pre-allocated seq. Returns the server's blobKey — the matching
36
+ * `rrweb` event row carries it.
37
+ */
38
+ export declare function uploadChunk(sessionId: string, seq: number, body: string): Promise<{
39
+ blobKey?: string;
40
+ }>;