demotape 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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +416 -0
  3. package/bin/demotape.mjs +6 -0
  4. package/configs/dogfood.json +51 -0
  5. package/configs/examples/instagram-story.json +23 -0
  6. package/configs/examples/landing-page.json +23 -0
  7. package/configs/examples/product-hunt.json +14 -0
  8. package/dist/auth/cookies.d.ts +7 -0
  9. package/dist/auth/cookies.d.ts.map +1 -0
  10. package/dist/auth/cookies.js +19 -0
  11. package/dist/auth/cookies.js.map +1 -0
  12. package/dist/auth/index.d.ts +25 -0
  13. package/dist/auth/index.d.ts.map +1 -0
  14. package/dist/auth/index.js +39 -0
  15. package/dist/auth/index.js.map +1 -0
  16. package/dist/auth/local-storage.d.ts +8 -0
  17. package/dist/auth/local-storage.d.ts.map +1 -0
  18. package/dist/auth/local-storage.js +14 -0
  19. package/dist/auth/local-storage.js.map +1 -0
  20. package/dist/auth/supabase.d.ts +8 -0
  21. package/dist/auth/supabase.d.ts.map +1 -0
  22. package/dist/auth/supabase.js +114 -0
  23. package/dist/auth/supabase.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +165 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/config.d.ts +562 -0
  29. package/dist/config.d.ts.map +1 -0
  30. package/dist/config.js +108 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/ffmpeg.d.ts +31 -0
  33. package/dist/ffmpeg.d.ts.map +1 -0
  34. package/dist/ffmpeg.js +55 -0
  35. package/dist/ffmpeg.js.map +1 -0
  36. package/dist/index.d.ts +4 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +5 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/overlays.d.ts +20 -0
  41. package/dist/overlays.d.ts.map +1 -0
  42. package/dist/overlays.js +60 -0
  43. package/dist/overlays.js.map +1 -0
  44. package/dist/recorder.d.ts +13 -0
  45. package/dist/recorder.d.ts.map +1 -0
  46. package/dist/recorder.js +155 -0
  47. package/dist/recorder.js.map +1 -0
  48. package/dist/scroll.d.ts +7 -0
  49. package/dist/scroll.d.ts.map +1 -0
  50. package/dist/scroll.js +14 -0
  51. package/dist/scroll.js.map +1 -0
  52. package/dist/segments.d.ts +21 -0
  53. package/dist/segments.d.ts.map +1 -0
  54. package/dist/segments.js +62 -0
  55. package/dist/segments.js.map +1 -0
  56. package/dist/utils.d.ts +25 -0
  57. package/dist/utils.d.ts.map +1 -0
  58. package/dist/utils.js +120 -0
  59. package/dist/utils.js.map +1 -0
  60. package/package.json +67 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jan Faris
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 ADDED
@@ -0,0 +1,416 @@
1
+ # demotape
2
+
3
+ **Automated demo videos from your live web app. JSON config in, polished MP4 out.**
4
+
5
+ Stop manually re-recording your demo video every time you change a button. Define page segments, scroll choreography, and text overlays in a JSON config — get pixel-perfect, skeleton-free videos for landing pages, Product Hunt, Instagram Stories, and docs.
6
+
7
+ ```bash
8
+ npx demotape init
9
+ # edit demotape.json with your app's URL and pages
10
+ npx demotape record --config demotape.json
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Why demotape?
16
+
17
+ | Problem | demotape solution |
18
+ |---------|-------------------|
19
+ | Re-record manually after every UI change | Run one command, get an updated video |
20
+ | Loading skeletons ruin the recording | Trims loading frames per-segment automatically |
21
+ | App is behind login | Auth-aware — Supabase, cookies, localStorage |
22
+ | Need different formats (landing page, IG Stories) | Multi-format from one config |
23
+ | Can't automate in CI/CD | Runs headlessly, updates videos on deploy |
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install -g demotape
29
+
30
+ # Playwright browsers (one-time)
31
+ npx playwright install chromium
32
+
33
+ # FFmpeg (required for encoding)
34
+ brew install ffmpeg # macOS
35
+ sudo apt install ffmpeg # Ubuntu/Debian
36
+ ```
37
+
38
+ > **Requires:** Node.js >= 18, FFmpeg, Playwright
39
+
40
+ ## Quick Start
41
+
42
+ ### 1. Generate a starter config
43
+
44
+ ```bash
45
+ demotape init
46
+ ```
47
+
48
+ This creates `demotape.json` with a landing page preset:
49
+
50
+ ```json
51
+ {
52
+ "baseUrl": "http://localhost:3000",
53
+ "viewport": { "width": 1280, "height": 800 },
54
+ "output": { "format": "both", "name": "demo" },
55
+ "colorScheme": "dark",
56
+ "segments": [
57
+ {
58
+ "name": "Home",
59
+ "path": "/",
60
+ "waitFor": "h1",
61
+ "settleMs": 1500,
62
+ "dwellMs": 3000
63
+ },
64
+ {
65
+ "name": "Dashboard",
66
+ "path": "/dashboard",
67
+ "waitFor": "main",
68
+ "settleMs": 2000,
69
+ "scroll": { "distance": 400, "duration": 2500 },
70
+ "dwellMs": 1500
71
+ }
72
+ ]
73
+ }
74
+ ```
75
+
76
+ ### 2. Edit the config
77
+
78
+ Point `baseUrl` to your running app. Add segments for each page you want to show.
79
+
80
+ ### 3. Record
81
+
82
+ ```bash
83
+ demotape record --config demotape.json
84
+ ```
85
+
86
+ Output lands in `./videos/demo.mp4` (and `demo.webm` if format is `"both"`).
87
+
88
+ ## How It Works
89
+
90
+ demotape uses **segment-based recording** to produce clean, skeleton-free videos:
91
+
92
+ 1. **Authenticate** — Logs into your app if auth is configured
93
+ 2. **Setup** — Sets localStorage keys to dismiss banners, onboarding, etc.
94
+ 3. **Warmup** — Visits every page once to prime the browser HTTP cache (images, fonts)
95
+ 4. **Record** — Opens each segment as a new page, waits for content to render, then records the scroll/dwell actions. Measures the loading time per segment.
96
+ 5. **Encode** — FFmpeg trims the loading frames from each segment, concatenates them, scales to output size, applies text overlays, and encodes to MP4/WebM
97
+
98
+ The result: every frame in the final video shows fully rendered content. No spinners, no skeleton screens, no progressive image loading.
99
+
100
+ ## Config Reference
101
+
102
+ ### Top-level fields
103
+
104
+ | Field | Type | Default | Description |
105
+ |-------|------|---------|-------------|
106
+ | `baseUrl` | `string` | *required* | Base URL of your app (e.g. `http://localhost:3000`) |
107
+ | `auth` | `object` | — | Authentication config (see [Auth Providers](#auth-providers)) |
108
+ | `viewport` | `{width, height}` | `1280x800` | Browser viewport size (CSS pixels) |
109
+ | `output` | `object` | — | Output config (see below) |
110
+ | `colorScheme` | `"dark" \| "light"` | `"dark"` | Browser color scheme |
111
+ | `removeDevOverlays` | `boolean` | `true` | Remove Next.js, PostHog, Vercel overlays |
112
+ | `suppressAnimations` | `boolean` | `true` | Disable CSS transitions/animations |
113
+ | `setup` | `object` | — | Pre-recording setup (see below) |
114
+ | `overlays` | `object` | — | Text overlays burned into the video |
115
+ | `segments` | `array` | *required* | Pages to record |
116
+
117
+ ### `output`
118
+
119
+ | Field | Type | Default | Description |
120
+ |-------|------|---------|-------------|
121
+ | `size` | `{width, height}` | same as viewport | Final video dimensions (FFmpeg scales up) |
122
+ | `format` | `"mp4" \| "webm" \| "both"` | `"mp4"` | Output format(s) |
123
+ | `fps` | `number` | `30` | Frames per second |
124
+ | `crf` | `number` | `28` | Quality (0-51, lower = better, bigger file) |
125
+ | `name` | `string` | `"demo"` | Output filename (without extension) |
126
+ | `dir` | `string` | `"./videos"` | Output directory |
127
+
128
+ ### `setup`
129
+
130
+ | Field | Type | Description |
131
+ |-------|------|-------------|
132
+ | `localStorage` | `Record<string, string>` | Key-value pairs to set before recording (dismiss banners, set theme, etc.) |
133
+ | `waitAfterSetup` | `number` | Milliseconds to wait after setup |
134
+
135
+ ### `overlays`
136
+
137
+ Text bands burned into the video via FFmpeg (useful for Instagram Stories, branded videos):
138
+
139
+ | Field | Type | Description |
140
+ |-------|------|-------------|
141
+ | `top` | `{text, height?, fontSize?}` | Top overlay band (default height: 120, fontSize: 42) |
142
+ | `bottom` | `{text, height?, fontSize?}` | Bottom overlay band (default height: 100, fontSize: 32) |
143
+
144
+ ### `segments[]`
145
+
146
+ Each segment records one page of your app:
147
+
148
+ | Field | Type | Default | Description |
149
+ |-------|------|---------|-------------|
150
+ | `name` | `string` | *required* | Display name for logging |
151
+ | `path` | `string` | *required* | URL path (e.g. `/dashboard`) |
152
+ | `waitFor` | `string` | — | CSS selector to wait for before recording |
153
+ | `settleMs` | `number` | `1000` | Ms to wait after content loads |
154
+ | `scroll` | `{distance, duration?}` | — | Scroll down by `distance` px over `duration` ms |
155
+ | `dwellMs` | `number` | `2000` | Ms to hold after all actions |
156
+ | `actions` | `array` | — | Click/hover actions before scroll |
157
+
158
+ #### `segments[].actions[]`
159
+
160
+ | Field | Type | Description |
161
+ |-------|------|-------------|
162
+ | `type` | `"click" \| "hover"` | Action type |
163
+ | `selector` | `string` | CSS selector for the target element |
164
+ | `delay` | `number` | Ms to wait before executing |
165
+
166
+ ## Auth Providers
167
+
168
+ demotape can record apps behind login. Configure auth in your JSON config.
169
+
170
+ ### Supabase (magic link)
171
+
172
+ Generates a magic link via Supabase admin API and injects session cookies. No `@supabase/supabase-js` dependency needed — uses raw `fetch()`.
173
+
174
+ ```json
175
+ {
176
+ "auth": {
177
+ "provider": "supabase",
178
+ "supabaseUrl": "https://abc.supabase.co",
179
+ "supabaseServiceRoleKey": "your-service-role-key",
180
+ "supabaseAnonKey": "your-anon-key",
181
+ "email": "demo@yourapp.com"
182
+ }
183
+ }
184
+ ```
185
+
186
+ Or use environment variables (recommended for secrets):
187
+
188
+ ```bash
189
+ export DEMOTAPE_SUPABASE_URL=https://abc.supabase.co
190
+ export DEMOTAPE_SUPABASE_ANON_KEY=your-anon-key
191
+ export DEMOTAPE_SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
192
+ export DEMOTAPE_EMAIL=demo@yourapp.com
193
+ ```
194
+
195
+ ```json
196
+ {
197
+ "auth": {
198
+ "provider": "supabase"
199
+ }
200
+ }
201
+ ```
202
+
203
+ ### Cookies
204
+
205
+ Inject raw cookies (works with any cookie-based auth):
206
+
207
+ ```json
208
+ {
209
+ "auth": {
210
+ "provider": "cookies",
211
+ "cookies": [
212
+ { "name": "session", "value": "abc123", "domain": "localhost" },
213
+ { "name": "token", "value": "xyz789", "domain": "localhost" }
214
+ ]
215
+ }
216
+ }
217
+ ```
218
+
219
+ ### localStorage
220
+
221
+ Inject localStorage key-value pairs (works with JWT-based auth that stores tokens in localStorage):
222
+
223
+ ```json
224
+ {
225
+ "auth": {
226
+ "provider": "localStorage",
227
+ "localStorage": {
228
+ "auth_token": "eyJhbGciOiJIUzI1NiIs...",
229
+ "user_id": "123"
230
+ }
231
+ }
232
+ }
233
+ ```
234
+
235
+ ## Presets
236
+
237
+ Generate starter configs for common use cases:
238
+
239
+ ```bash
240
+ # Landscape for landing pages (1280x800, MP4+WebM)
241
+ demotape init --preset landing-page
242
+
243
+ # Vertical for Instagram Stories (1080x1920 with text overlays)
244
+ demotape init --preset instagram-story
245
+
246
+ # 16:9 for Product Hunt (1920x1080)
247
+ demotape init --preset product-hunt
248
+ ```
249
+
250
+ List all presets:
251
+
252
+ ```bash
253
+ demotape presets
254
+ ```
255
+
256
+ ## CI/CD
257
+
258
+ ### GitHub Actions
259
+
260
+ Automatically re-record your demo video on every deploy:
261
+
262
+ ```yaml
263
+ name: Record Demo Video
264
+ on:
265
+ push:
266
+ branches: [main]
267
+
268
+ jobs:
269
+ record:
270
+ runs-on: ubuntu-latest
271
+ steps:
272
+ - uses: actions/checkout@v4
273
+
274
+ - uses: actions/setup-node@v4
275
+ with:
276
+ node-version: 20
277
+
278
+ - name: Install dependencies
279
+ run: |
280
+ npm ci
281
+ npx playwright install chromium --with-deps
282
+ sudo apt-get install -y ffmpeg
283
+
284
+ - name: Start app
285
+ run: npm run dev &
286
+ env:
287
+ PORT: 3000
288
+
289
+ - name: Wait for app
290
+ run: npx wait-on http://localhost:3000
291
+
292
+ - name: Record demo
293
+ run: npx demotape record --config demotape.json
294
+ env:
295
+ HEADLESS: true
296
+ DEMOTAPE_SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
297
+ DEMOTAPE_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
298
+ DEMOTAPE_SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
299
+ DEMOTAPE_EMAIL: ${{ secrets.DEMO_EMAIL }}
300
+
301
+ - name: Upload video
302
+ uses: actions/upload-artifact@v4
303
+ with:
304
+ name: demo-video
305
+ path: videos/
306
+ ```
307
+
308
+ ## CLI Reference
309
+
310
+ ```bash
311
+ # Record using a config file
312
+ demotape record --config demotape.json
313
+
314
+ # Record with overrides
315
+ demotape record --config demotape.json --format webm --output ./dist
316
+
317
+ # Generate a starter config
318
+ demotape init
319
+ demotape init --preset instagram-story
320
+ demotape init --preset product-hunt
321
+
322
+ # Validate a config without recording
323
+ demotape validate --config demotape.json
324
+
325
+ # Show available presets
326
+ demotape presets
327
+
328
+ # Show help
329
+ demotape --help
330
+ ```
331
+
332
+ ## Programmatic API
333
+
334
+ Use demotape as a library in your own scripts:
335
+
336
+ ```typescript
337
+ import { record, loadConfig } from "demotape";
338
+
339
+ const config = loadConfig("./demotape.json");
340
+ await record(config);
341
+ ```
342
+
343
+ Or build a config object directly:
344
+
345
+ ```typescript
346
+ import { record, type DemotapeConfig } from "demotape";
347
+
348
+ const config: DemotapeConfig = {
349
+ baseUrl: "http://localhost:3000",
350
+ viewport: { width: 1280, height: 800 },
351
+ output: { format: "mp4", fps: 30, crf: 28, name: "demo", dir: "./videos" },
352
+ colorScheme: "dark",
353
+ removeDevOverlays: true,
354
+ suppressAnimations: true,
355
+ segments: [
356
+ { name: "Home", path: "/", waitFor: "h1", settleMs: 1500, dwellMs: 3000 },
357
+ ],
358
+ };
359
+
360
+ await record(config);
361
+ ```
362
+
363
+ ## Environment Variables
364
+
365
+ | Variable | Description |
366
+ |----------|-------------|
367
+ | `DEMOTAPE_SUPABASE_URL` | Supabase project URL |
368
+ | `DEMOTAPE_SUPABASE_ANON_KEY` | Supabase anon/public key |
369
+ | `DEMOTAPE_SUPABASE_SERVICE_ROLE_KEY` | Supabase service role key |
370
+ | `DEMOTAPE_EMAIL` | Demo account email for Supabase auth |
371
+ | `HEADLESS` | Set to `"false"` to see the browser during recording |
372
+
373
+ ## FAQ
374
+
375
+ ### Do I need FFmpeg?
376
+
377
+ Yes. demotape uses FFmpeg to trim loading frames, concatenate segments, apply overlays, and encode the final video. Install it with `brew install ffmpeg` (macOS) or `sudo apt install ffmpeg` (Linux).
378
+
379
+ ### Why are there gray bars in my video?
380
+
381
+ This happens when `recordVideo.size` doesn't match the viewport. demotape handles this automatically — it sets both to the same value and uses FFmpeg to scale up to the output size. If you see gray bars, make sure `output.size` is a multiple of your `viewport` dimensions.
382
+
383
+ ### Can I record apps that need authentication?
384
+
385
+ Yes. demotape supports three auth providers: Supabase (magic link), raw cookies, and localStorage injection. See [Auth Providers](#auth-providers).
386
+
387
+ ### How do I dismiss banners/modals before recording?
388
+
389
+ Use the `setup.localStorage` field to set keys that your app checks. For example, if your app hides an onboarding modal when `onboarding-done` is in localStorage:
390
+
391
+ ```json
392
+ {
393
+ "setup": {
394
+ "localStorage": {
395
+ "onboarding-done": "1",
396
+ "cookie-consent": "accepted"
397
+ }
398
+ }
399
+ }
400
+ ```
401
+
402
+ ### Can I run this in CI/CD?
403
+
404
+ Yes. Set `HEADLESS=true` (the default) and make sure Playwright browsers and FFmpeg are installed. See [CI/CD](#cicd) for a GitHub Actions example.
405
+
406
+ ### How do I reduce file size?
407
+
408
+ Increase the `crf` value in your output config. The default is 28. Try 32-35 for smaller files with slightly lower quality.
409
+
410
+ ### Why is the first frame of a segment blurry?
411
+
412
+ The warmup phase should prevent this by priming the browser cache. If you still see blurry first frames, increase `settleMs` for that segment to give images more time to load.
413
+
414
+ ## License
415
+
416
+ MIT
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createCLI } from "../dist/cli.js";
4
+
5
+ const program = createCLI();
6
+ program.parse();
@@ -0,0 +1,51 @@
1
+ {
2
+ "baseUrl": "http://localhost:5500/site",
3
+ "viewport": { "width": 1280, "height": 800 },
4
+ "output": {
5
+ "format": "both",
6
+ "name": "demotape-demo",
7
+ "dir": "./videos"
8
+ },
9
+ "colorScheme": "dark",
10
+ "segments": [
11
+ {
12
+ "name": "Hero",
13
+ "path": "/index.html",
14
+ "waitFor": "h1",
15
+ "settleMs": 2000,
16
+ "dwellMs": 4000
17
+ },
18
+ {
19
+ "name": "Before/After",
20
+ "path": "/index.html",
21
+ "waitFor": ".comparison-row",
22
+ "settleMs": 1500,
23
+ "scroll": { "distance": 900, "duration": 3000 },
24
+ "dwellMs": 2000
25
+ },
26
+ {
27
+ "name": "Config Preview",
28
+ "path": "/index.html",
29
+ "waitFor": ".syn-key",
30
+ "settleMs": 1500,
31
+ "scroll": { "distance": 1800, "duration": 4000 },
32
+ "dwellMs": 2000
33
+ },
34
+ {
35
+ "name": "Features",
36
+ "path": "/index.html",
37
+ "waitFor": ".feature-card",
38
+ "settleMs": 1500,
39
+ "scroll": { "distance": 2800, "duration": 5000 },
40
+ "dwellMs": 2000
41
+ },
42
+ {
43
+ "name": "Pricing",
44
+ "path": "/index.html",
45
+ "waitFor": ".pricing-popular",
46
+ "settleMs": 1500,
47
+ "scroll": { "distance": 3800, "duration": 5000 },
48
+ "dwellMs": 3000
49
+ }
50
+ ]
51
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "baseUrl": "http://localhost:3000",
3
+ "viewport": { "width": 540, "height": 960 },
4
+ "output": {
5
+ "size": { "width": 1080, "height": 1920 },
6
+ "format": "mp4",
7
+ "name": "story"
8
+ },
9
+ "overlays": {
10
+ "top": { "text": "Your App Name", "height": 120 },
11
+ "bottom": { "text": "Try it free", "height": 100 }
12
+ },
13
+ "segments": [
14
+ {
15
+ "name": "Feature Page",
16
+ "path": "/features",
17
+ "waitFor": "h1",
18
+ "settleMs": 2000,
19
+ "scroll": { "distance": 800, "duration": 5000 },
20
+ "dwellMs": 2000
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "baseUrl": "http://localhost:3000",
3
+ "viewport": { "width": 1280, "height": 800 },
4
+ "output": { "format": "both", "name": "demo" },
5
+ "colorScheme": "dark",
6
+ "segments": [
7
+ {
8
+ "name": "Home",
9
+ "path": "/",
10
+ "waitFor": "h1",
11
+ "settleMs": 1500,
12
+ "dwellMs": 3000
13
+ },
14
+ {
15
+ "name": "Dashboard",
16
+ "path": "/dashboard",
17
+ "waitFor": "main",
18
+ "settleMs": 2000,
19
+ "scroll": { "distance": 400, "duration": 2500 },
20
+ "dwellMs": 1500
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "baseUrl": "http://localhost:3000",
3
+ "viewport": { "width": 1920, "height": 1080 },
4
+ "output": { "format": "mp4", "name": "product-hunt-demo" },
5
+ "segments": [
6
+ {
7
+ "name": "Hero",
8
+ "path": "/",
9
+ "waitFor": "h1",
10
+ "settleMs": 2000,
11
+ "dwellMs": 4000
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,7 @@
1
+ import type { AuthConfig } from "../config.js";
2
+ import type { AuthResult } from "./index.js";
3
+ /**
4
+ * Build auth result from raw cookie definitions in the config.
5
+ */
6
+ export declare function injectCookies(auth: AuthConfig): AuthResult;
7
+ //# sourceMappingURL=cookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/auth/cookies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAgB1D"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Build auth result from raw cookie definitions in the config.
3
+ */
4
+ export function injectCookies(auth) {
5
+ if (!auth.cookies || auth.cookies.length === 0) {
6
+ throw new Error("Cookie auth provider requires `cookies` array in config");
7
+ }
8
+ const cookies = auth.cookies.map((c) => ({
9
+ name: c.name,
10
+ value: c.value,
11
+ domain: c.domain ?? "localhost",
12
+ path: c.path ?? "/",
13
+ httpOnly: false,
14
+ secure: false,
15
+ sameSite: "Lax",
16
+ }));
17
+ return { cookies };
18
+ }
19
+ //# sourceMappingURL=cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/auth/cookies.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,IAAgB;IAC5C,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IAED,MAAM,OAAO,GAA0B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9D,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,WAAW;QAC/B,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,GAAG;QACnB,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,KAAc;KACzB,CAAC,CAAC,CAAC;IAEJ,OAAO,EAAE,OAAO,EAAE,CAAC;AACrB,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { BrowserContext } from "playwright";
2
+ import type { AuthConfig } from "../config.js";
3
+ export interface AuthResult {
4
+ cookies: Array<{
5
+ name: string;
6
+ value: string;
7
+ domain: string;
8
+ path: string;
9
+ httpOnly?: boolean;
10
+ secure?: boolean;
11
+ sameSite?: "Strict" | "Lax" | "None";
12
+ }>;
13
+ localStorage?: Record<string, string>;
14
+ }
15
+ /**
16
+ * Authenticate using the configured provider and return cookies/localStorage
17
+ * to inject into the browser context.
18
+ */
19
+ export declare function authenticate(auth: AuthConfig, baseUrl: string): Promise<AuthResult>;
20
+ /**
21
+ * Apply auth result to a browser context — sets cookies and optionally
22
+ * injects localStorage values.
23
+ */
24
+ export declare function applyAuth(context: BrowserContext, authResult: AuthResult, baseUrl: string): Promise<void>;
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK/C,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;KACtC,CAAC,CAAC;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,CAAC,CAWrB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAef"}
@@ -0,0 +1,39 @@
1
+ import { authenticateSupabase } from "./supabase.js";
2
+ import { injectCookies } from "./cookies.js";
3
+ import { injectLocalStorage } from "./local-storage.js";
4
+ /**
5
+ * Authenticate using the configured provider and return cookies/localStorage
6
+ * to inject into the browser context.
7
+ */
8
+ export async function authenticate(auth, baseUrl) {
9
+ switch (auth.provider) {
10
+ case "supabase":
11
+ return authenticateSupabase(auth, baseUrl);
12
+ case "cookies":
13
+ return injectCookies(auth);
14
+ case "localStorage":
15
+ return injectLocalStorage(auth);
16
+ default:
17
+ throw new Error(`Unknown auth provider: ${auth.provider}`);
18
+ }
19
+ }
20
+ /**
21
+ * Apply auth result to a browser context — sets cookies and optionally
22
+ * injects localStorage values.
23
+ */
24
+ export async function applyAuth(context, authResult, baseUrl) {
25
+ if (authResult.cookies.length > 0) {
26
+ await context.addCookies(authResult.cookies);
27
+ }
28
+ if (authResult.localStorage && Object.keys(authResult.localStorage).length > 0) {
29
+ const page = await context.newPage();
30
+ await page.goto(baseUrl, { waitUntil: "domcontentloaded" });
31
+ await page.evaluate((items) => {
32
+ for (const [key, value] of Object.entries(items)) {
33
+ localStorage.setItem(key, value);
34
+ }
35
+ }, authResult.localStorage);
36
+ await page.close();
37
+ }
38
+ }
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAexD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAgB,EAChB,OAAe;IAEf,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,KAAK,UAAU;YACb,OAAO,oBAAoB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC7C,KAAK,SAAS;YACZ,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;QAC7B,KAAK,cAAc;YACjB,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAClC;YACE,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,OAAuB,EACvB,UAAsB,EACtB,OAAe;IAEf,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,MAAM,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,UAAU,CAAC,YAAY,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/E,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAC5D,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE;YAC5B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACjD,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACnC,CAAC;QACH,CAAC,EAAE,UAAU,CAAC,YAAY,CAAC,CAAC;QAC5B,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { AuthConfig } from "../config.js";
2
+ import type { AuthResult } from "./index.js";
3
+ /**
4
+ * Build auth result from localStorage key-value pairs.
5
+ * These get injected into the browser via page.evaluate().
6
+ */
7
+ export declare function injectLocalStorage(auth: AuthConfig): AuthResult;
8
+ //# sourceMappingURL=local-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-storage.d.ts","sourceRoot":"","sources":["../../src/auth/local-storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAW/D"}