@growth-labs/video 0.3.13 → 0.4.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @growth-labs/video
2
2
 
3
- Astro 6 integration for HLS video playback from R2, reusable R2 HLS bundle ingestion, captions/chapters sidecars, YouTube embeds, and optional Worker-proxied premium gating.
3
+ Astro 6 integration for HLS video playback from R2, reusable R2 HLS bundle ingestion, captions/chapters sidecars, YouTube embeds, and Worker-proxied premium gating from a distinct private R2 origin.
4
4
 
5
5
  ## Install
6
6
 
@@ -26,14 +26,16 @@ video({
26
26
  })
27
27
  ```
28
28
 
29
- Premium playback additionally needs `premium.enabled`, an `accessCheckModule` (a module-spec pointing at a file in your own codebase), and a `GL_VIDEO_SESSION_SECRET` secret of at least 32 bytes:
29
+ Premium playback additionally needs `premium.enabled`, a distinct private R2 binding, an `accessCheckModule` (a module-spec pointing at a file in your own codebase), and a `GL_VIDEO_SESSION_SECRET` secret of at least 32 bytes:
30
30
 
31
31
  ```ts
32
32
  // astro.config.mjs
33
33
  video({
34
34
  publicDomain: 'media.example.com',
35
+ r2Binding: 'MEDIA_BUCKET', // public images and public video only
35
36
  premium: {
36
37
  enabled: true,
38
+ r2Binding: 'PREMIUM_MEDIA', // private bucket; no r2.dev or custom domain
37
39
  sessionScope: 'per-video', // default; 'wildcard' opts into the 0.2.x behavior
38
40
  sessionTtl: '1h', // duration string: '1h' | '30m' | '2h30m' | '45s'
39
41
  accessCheckModule: { module: '/src/lib/video-access-check', export: 'check' },
@@ -41,6 +43,27 @@ video({
41
43
  })
42
44
  ```
43
45
 
46
+ `premium.r2Binding` is required when `premium.enabled=true` and must differ from
47
+ the public `r2Binding`. The package rejects a shared binding at build time. A
48
+ different binding name is necessary but not sufficient: the consumer must bind
49
+ it to a genuinely separate bucket with both `r2.dev` and custom-domain access
50
+ disabled. The package reads protected objects only through the Worker binding;
51
+ the signed per-video session cookie is resource-bound and expires.
52
+
53
+ ```toml
54
+ [[r2_buckets]]
55
+ binding = "MEDIA_BUCKET"
56
+ bucket_name = "<site>-media"
57
+
58
+ [[r2_buckets]]
59
+ binding = "PREMIUM_MEDIA"
60
+ bucket_name = "<site>-premium-media"
61
+ ```
62
+
63
+ Provisioning buckets, bindings, domains, and the Secrets Store entry is an
64
+ operator-owned deployment step. This package release does not mutate those
65
+ surfaces.
66
+
44
67
  ```ts
45
68
  // /src/lib/video-access-check.ts
46
69
  import type { AccessCheck } from '@growth-labs/video'
@@ -114,8 +137,38 @@ await writeCaptionsVtt({ bucket: env.MEDIA_BUCKET, videoId: 'intro-video', vtt:
114
137
  await writeChaptersVtt({ bucket: env.MEDIA_BUCKET, videoId: 'intro-video', vtt: chaptersVtt })
115
138
  ```
116
139
 
117
- The ingestion helper validates `master.m3u8`, referenced rendition playlists, segment paths, and local HLS `URI="..."` attributes such as `EXT-X-MAP`, `EXT-X-MEDIA`, `EXT-X-I-FRAME-STREAM-INF`, and `EXT-X-KEY` before writing to R2. Absolute/external URLs and unsafe relative paths are rejected. Transcoding is out of scope; pass already-generated HLS and VTT files.
140
+ The ingestion helper is the public-bundle path: it validates `master.m3u8`, referenced rendition playlists, segment paths, and local HLS `URI="..."` attributes such as `EXT-X-MAP`, `EXT-X-MEDIA`, `EXT-X-I-FRAME-STREAM-INF`, and `EXT-X-KEY` before writing to R2 and returning a public manifest URL. Protected bundles are written by the owner publishing pipeline to `premium.r2Binding`; never persist or expose a public origin URL for them. Absolute/external URLs and unsafe relative paths are rejected. Transcoding is out of scope; pass already-generated HLS and VTT files.
118
141
 
119
142
  All R2 key helpers normalize `videoId` with the same safe contract: IDs must start with an ASCII letter or number and then contain only ASCII letters, numbers, `_`, or `-`. Text-track helpers write only under `video/<videoId>/text/captions_<lang>.vtt` and `video/<videoId>/text/chapters.vtt`.
120
143
 
121
144
  Injected routes read bindings from `cloudflare:workers` `env`; no `locals.runtime` shim is required.
145
+
146
+ ## Premium access verification
147
+
148
+ The repository-level verifier proves the deployed route and legacy origin in a
149
+ single redacted run. `PREMIUM_HLS_TEST_FIXTURE` contains an absolute path to a
150
+ regular JSON file with mode `0600`; it never contains the fixture value itself.
151
+
152
+ ```json
153
+ {
154
+ "masterUrl": "https://site.example/api/video/playback/<resource>/hls/master.m3u8",
155
+ "authorizedCookie": "gl_video_session=<owner-provisioned-authorized-cookie>",
156
+ "expiredCookie": "gl_video_session=<owner-provisioned-expired-cookie>",
157
+ "wrongAudienceCookie": "gl_video_session=<owner-provisioned-wrong-resource-cookie>",
158
+ "nonEntitledCookie": "fronts_session=<owner-provisioned-non-entitled-session>",
159
+ "publicOriginMasterUrl": "https://media.example/video/<resource>/hls/master.m3u8",
160
+ "publicOriginSegmentUrl": "https://media.example/video/<resource>/hls/<rendition>/<segment>"
161
+ }
162
+ ```
163
+
164
+ ```bash
165
+ pnpm run hls:access:verify -- --fixture-env PREMIUM_HLS_TEST_FIXTURE
166
+ ```
167
+
168
+ The verifier requires 403 for anonymous, expired, wrong-audience, and
169
+ non-entitled gateway requests, 200 for authorized playlists/segments, 206 for
170
+ an authorized range, and 403 or 404 for the corresponding unsigned
171
+ legacy-origin objects. It follows no redirects,
172
+ rejects cross-origin and path-escaping manifest references, bounds all manifest
173
+ reads, and prints only named checks and status codes—never URLs, paths, cookies,
174
+ manifests, or media bytes.
package/dist/options.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- export declare const videoOptionsSchema: z.ZodObject<{
2
+ export declare const videoOptionsSchema: z.ZodEffects<z.ZodObject<{
3
3
  publicDomain: z.ZodString;
4
4
  r2Binding: z.ZodDefault<z.ZodString>;
5
5
  admin: z.ZodDefault<z.ZodObject<{
@@ -31,6 +31,7 @@ export declare const videoOptionsSchema: z.ZodObject<{
31
31
  }>>;
32
32
  premium: z.ZodOptional<z.ZodObject<{
33
33
  enabled: z.ZodDefault<z.ZodBoolean>;
34
+ r2Binding: z.ZodOptional<z.ZodString>;
34
35
  sessionRoute: z.ZodDefault<z.ZodString>;
35
36
  playbackRoute: z.ZodDefault<z.ZodString>;
36
37
  sessionCookieName: z.ZodDefault<z.ZodString>;
@@ -55,11 +56,13 @@ export declare const videoOptionsSchema: z.ZodObject<{
55
56
  sessionScope: "per-video" | "wildcard";
56
57
  sessionTtl: number;
57
58
  signingSecretEnv: string;
59
+ r2Binding?: string | undefined;
58
60
  accessCheckModule?: {
59
61
  module: string;
60
62
  export: string;
61
63
  } | undefined;
62
64
  }, {
65
+ r2Binding?: string | undefined;
63
66
  enabled?: boolean | undefined;
64
67
  sessionRoute?: string | undefined;
65
68
  playbackRoute?: string | undefined;
@@ -156,6 +159,7 @@ export declare const videoOptionsSchema: z.ZodObject<{
156
159
  sessionScope: "per-video" | "wildcard";
157
160
  sessionTtl: number;
158
161
  signingSecretEnv: string;
162
+ r2Binding?: string | undefined;
159
163
  accessCheckModule?: {
160
164
  module: string;
161
165
  export: string;
@@ -172,6 +176,91 @@ export declare const videoOptionsSchema: z.ZodObject<{
172
176
  } | undefined;
173
177
  } | undefined;
174
178
  premium?: {
179
+ r2Binding?: string | undefined;
180
+ enabled?: boolean | undefined;
181
+ sessionRoute?: string | undefined;
182
+ playbackRoute?: string | undefined;
183
+ sessionCookieName?: string | undefined;
184
+ sessionScope?: "per-video" | "wildcard" | undefined;
185
+ sessionTtl?: string | undefined;
186
+ signingSecretEnv?: string | undefined;
187
+ accessCheckModule?: {
188
+ module: string;
189
+ export?: string | undefined;
190
+ } | undefined;
191
+ } | undefined;
192
+ player?: {
193
+ autoplay?: boolean | undefined;
194
+ muted?: boolean | undefined;
195
+ preload?: "none" | "metadata" | "auto" | undefined;
196
+ showCaptions?: boolean | undefined;
197
+ showChapters?: boolean | undefined;
198
+ controls?: boolean | undefined;
199
+ startingQuality?: "auto" | "highest" | "lowest" | undefined;
200
+ } | undefined;
201
+ youtube?: {
202
+ enabled?: boolean | undefined;
203
+ privacyMode?: boolean | undefined;
204
+ lazyLoad?: boolean | undefined;
205
+ } | undefined;
206
+ analytics?: {
207
+ enabled?: boolean | undefined;
208
+ progressMarks?: number[] | undefined;
209
+ } | undefined;
210
+ }>, {
211
+ publicDomain: string;
212
+ r2Binding: string;
213
+ admin: {
214
+ importRoute: {
215
+ enabled: boolean;
216
+ path: string;
217
+ requireAuth: boolean;
218
+ };
219
+ };
220
+ player: {
221
+ autoplay: boolean;
222
+ muted: boolean;
223
+ preload: "none" | "metadata" | "auto";
224
+ showCaptions: boolean;
225
+ showChapters: boolean;
226
+ controls: boolean;
227
+ startingQuality: "auto" | "highest" | "lowest";
228
+ };
229
+ youtube: {
230
+ enabled: boolean;
231
+ privacyMode: boolean;
232
+ lazyLoad: boolean;
233
+ };
234
+ analytics: {
235
+ enabled: boolean;
236
+ progressMarks: number[];
237
+ };
238
+ premium?: {
239
+ enabled: boolean;
240
+ sessionRoute: string;
241
+ playbackRoute: string;
242
+ sessionCookieName: string;
243
+ sessionScope: "per-video" | "wildcard";
244
+ sessionTtl: number;
245
+ signingSecretEnv: string;
246
+ r2Binding?: string | undefined;
247
+ accessCheckModule?: {
248
+ module: string;
249
+ export: string;
250
+ } | undefined;
251
+ } | undefined;
252
+ }, {
253
+ publicDomain: string;
254
+ r2Binding?: string | undefined;
255
+ admin?: {
256
+ importRoute?: {
257
+ enabled?: boolean | undefined;
258
+ path?: string | undefined;
259
+ requireAuth?: boolean | undefined;
260
+ } | undefined;
261
+ } | undefined;
262
+ premium?: {
263
+ r2Binding?: string | undefined;
175
264
  enabled?: boolean | undefined;
176
265
  sessionRoute?: string | undefined;
177
266
  playbackRoute?: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkG7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAC7D,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAA"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwH5B,CAAA;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAC7D,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAA"}
package/dist/options.js CHANGED
@@ -15,7 +15,8 @@ function parseDurationToSeconds(input) {
15
15
  const seconds = s ? Number.parseInt(s, 10) : 0;
16
16
  return hours * 3600 + minutes * 60 + seconds;
17
17
  }
18
- export const videoOptionsSchema = z.object({
18
+ export const videoOptionsSchema = z
19
+ .object({
19
20
  // ─── Required ───
20
21
  publicDomain: z.string(),
21
22
  // ─── R2 bindings ───
@@ -36,6 +37,7 @@ export const videoOptionsSchema = z.object({
36
37
  premium: z
37
38
  .object({
38
39
  enabled: z.boolean().default(false),
40
+ r2Binding: z.string().min(1).optional(),
39
41
  sessionRoute: z.string().default('/api/video/session'),
40
42
  playbackRoute: z.string().default('/api/video/playback'),
41
43
  sessionCookieName: z
@@ -107,5 +109,24 @@ export const videoOptionsSchema = z.object({
107
109
  progressMarks: z.array(z.number().min(0).max(1)).default([0.25, 0.5, 0.75, 0.95]),
108
110
  })
109
111
  .default({}),
112
+ })
113
+ .superRefine((options, ctx) => {
114
+ if (!options.premium?.enabled)
115
+ return;
116
+ if (!options.premium.r2Binding) {
117
+ ctx.addIssue({
118
+ code: z.ZodIssueCode.custom,
119
+ path: ['premium', 'r2Binding'],
120
+ message: 'premium.r2Binding is required when premium.enabled=true and must name a private R2 binding.',
121
+ });
122
+ return;
123
+ }
124
+ if (options.premium.r2Binding === options.r2Binding) {
125
+ ctx.addIssue({
126
+ code: z.ZodIssueCode.custom,
127
+ path: ['premium', 'r2Binding'],
128
+ message: 'premium.r2Binding must be a distinct private R2 binding; the public r2Binding cannot protect premium media.',
129
+ });
130
+ }
110
131
  });
111
132
  //# sourceMappingURL=options.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"playback.d.ts","sourceRoot":"","sources":["../../src/routes/playback.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AA+CrC,eAAO,MAAM,GAAG,EAAE,QAmDjB,CAAA"}
1
+ {"version":3,"file":"playback.d.ts","sourceRoot":"","sources":["../../src/routes/playback.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AA6DrC,eAAO,MAAM,GAAG,EAAE,QAoDjB,CAAA"}
@@ -35,48 +35,61 @@ function readCookie(header, name) {
35
35
  const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
36
36
  return match ? match[1] : null;
37
37
  }
38
+ function protectedResponse(body, init) {
39
+ const headers = new Headers(init.headers);
40
+ headers.set('cache-control', 'private, no-store');
41
+ const vary = new Set((headers.get('vary') ?? '')
42
+ .split(',')
43
+ .map((value) => value.trim())
44
+ .filter(Boolean));
45
+ vary.add('Cookie');
46
+ headers.set('vary', [...vary].join(', '));
47
+ return new Response(body, { ...init, headers });
48
+ }
38
49
  export const GET = async (context) => {
39
50
  const { request, params } = context;
40
51
  const env = cloudflareEnv;
41
52
  const rest = params.path;
42
53
  if (!rest || rest.length === 0) {
43
- return new Response('invalid path', { status: 400 });
54
+ return protectedResponse('invalid path', { status: 400 });
44
55
  }
45
56
  const segments = rest.split('/').filter(Boolean);
46
57
  if (segments.length < 2) {
47
- return new Response('invalid path', { status: 400 });
58
+ return protectedResponse('invalid path', { status: 400 });
48
59
  }
49
60
  if (segments.some((s) => s === '..' || s === '.')) {
50
- return new Response('invalid path', { status: 400 });
61
+ return protectedResponse('invalid path', { status: 400 });
51
62
  }
52
63
  if (!config.premium)
53
- return new Response('premium disabled', { status: 404 });
64
+ return protectedResponse('premium disabled', { status: 404 });
54
65
  const videoId = segments[0];
55
66
  const subKey = segments.slice(1).join('/');
56
67
  const cookie = readCookie(request.headers.get('cookie'), config.premium.sessionCookieName);
57
68
  if (!cookie)
58
- return new Response('unauthorized', { status: 401 });
69
+ return protectedResponse('forbidden', { status: 403 });
59
70
  const secret = env[config.premium.signingSecretEnv];
60
71
  if (!secret)
61
- return new Response('misconfigured', { status: 500 });
72
+ return protectedResponse('misconfigured', { status: 500 });
62
73
  const session = await verifySession(cookie, secret, videoId);
63
74
  if (!session)
64
- return new Response('forbidden', { status: 403 });
65
- const bucket = env[config.r2Binding];
75
+ return protectedResponse('forbidden', { status: 403 });
76
+ const premiumR2Binding = config.premium.r2Binding;
77
+ if (!premiumR2Binding)
78
+ return protectedResponse('misconfigured', { status: 500 });
79
+ const bucket = env[premiumR2Binding];
66
80
  if (!bucket)
67
- return new Response('misconfigured', { status: 500 });
81
+ return protectedResponse('misconfigured', { status: 500 });
68
82
  const rangeHeader = request.headers.get('range');
69
83
  const range = rangeHeader ? parseRangeHeader(rangeHeader) : undefined;
70
84
  const r2Key = `video/${videoId}/${subKey}`;
71
85
  const obj = await bucket.get(r2Key, range ? { range } : undefined);
72
86
  if (!obj)
73
- return new Response('not found', { status: 404 });
87
+ return protectedResponse('not found', { status: 404 });
74
88
  const headers = new Headers();
75
89
  obj.writeHttpMetadata(headers);
76
90
  headers.set('etag', obj.httpEtag);
77
91
  headers.set('content-type', inferContentType(r2Key));
78
- headers.set('cache-control', r2Key.endsWith('.m3u8') ? 'no-cache' : 'public, max-age=3600');
79
- return new Response(obj.body, {
92
+ return protectedResponse(obj.body, {
80
93
  status: range ? 206 : 200,
81
94
  headers,
82
95
  });
@@ -1 +1 @@
1
- {"version":3,"file":"playback.js","sourceRoot":"","sources":["../../src/routes/playback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAA;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAenD,SAAS,gBAAgB,CAAC,GAAW;IACpC,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,+BAA+B,CAAA;IACjE,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,mBAAmB,CAAA;IACpD,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,WAAW,CAAA;IAC5C,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,UAAU,CAAA;IAC3C,OAAO,0BAA0B,CAAA;AAClC,CAAC;AAED,SAAS,gBAAgB,CACxB,MAAc;IAEd,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAA;IAC7C,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IACxB,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAClB,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAChB,IAAI,KAAK,KAAK,EAAE,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAA;IACpE,IAAI,KAAK,KAAK,EAAE,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAA;IACtE,IAAI,KAAK,KAAK,EAAE,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QAC7B,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QAC3B,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAA;IACxC,CAAC;IACD,OAAO,SAAS,CAAA;AACjB,CAAC;AAED,SAAS,UAAU,CAAC,MAAqB,EAAE,IAAY;IACtD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACxB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,cAAc,IAAI,UAAU,CAAC,CAAC,CAAA;IACpE,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC/B,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9C,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IACnC,MAAM,GAAG,GAAG,aAAwC,CAAA;IAEpD,MAAM,IAAI,GAAG,MAAM,CAAC,IAA0B,CAAA;IAC9C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACrD,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAChD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACrD,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;QACnD,OAAO,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACrD,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAE7E,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAE1C,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC1F,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAEjE,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAuB,CAAA;IACzE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAElE,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;IAC5D,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAE/D,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,CAA6B,CAAA;IAChE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAElE,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAChD,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAErE,MAAM,KAAK,GAAG,SAAS,OAAO,IAAI,MAAM,EAAE,CAAA;IAC1C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAClE,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAE3D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;IAC7B,GAAG,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;IAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IACjC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAA;IACpD,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAA;IAE3F,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE;QAC7B,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;QACzB,OAAO;KACP,CAAC,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"playback.js","sourceRoot":"","sources":["../../src/routes/playback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAA;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAenD,SAAS,gBAAgB,CAAC,GAAW;IACpC,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,+BAA+B,CAAA;IACjE,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,mBAAmB,CAAA;IACpD,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,WAAW,CAAA;IAC5C,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,UAAU,CAAA;IAC3C,OAAO,0BAA0B,CAAA;AAClC,CAAC;AAED,SAAS,gBAAgB,CACxB,MAAc;IAEd,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAA;IAC7C,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IACxB,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAClB,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAChB,IAAI,KAAK,KAAK,EAAE,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAA;IACpE,IAAI,KAAK,KAAK,EAAE,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAA;IACtE,IAAI,KAAK,KAAK,EAAE,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QAC7B,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QAC3B,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAA;IACxC,CAAC;IACD,OAAO,SAAS,CAAA;AACjB,CAAC;AAED,SAAS,UAAU,CAAC,MAAqB,EAAE,IAAY;IACtD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACxB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,cAAc,IAAI,UAAU,CAAC,CAAC,CAAA;IACpE,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC/B,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAqB,EAAE,IAAkB;IACnE,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACzC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAA;IACjD,MAAM,IAAI,GAAG,IAAI,GAAG,CACnB,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;SACzB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC,CACjB,CAAA;IACD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACzC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;AAChD,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9C,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IACnC,MAAM,GAAG,GAAG,aAAwC,CAAA;IAEpD,MAAM,IAAI,GAAG,MAAM,CAAC,IAA0B,CAAA;IAC9C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,iBAAiB,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC1D,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAChD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,iBAAiB,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC1D,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;QACnD,OAAO,iBAAiB,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC1D,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,iBAAiB,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAElF,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAE1C,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC1F,IAAI,CAAC,MAAM;QAAE,OAAO,iBAAiB,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAEnE,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAuB,CAAA;IACzE,IAAI,CAAC,MAAM;QAAE,OAAO,iBAAiB,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAEvE,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;IAC5D,IAAI,CAAC,OAAO;QAAE,OAAO,iBAAiB,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAEpE,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,CAAA;IACjD,IAAI,CAAC,gBAAgB;QAAE,OAAO,iBAAiB,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAEjF,MAAM,MAAM,GAAG,GAAG,CAAC,gBAAgB,CAA6B,CAAA;IAChE,IAAI,CAAC,MAAM;QAAE,OAAO,iBAAiB,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAEvE,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAChD,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAErE,MAAM,KAAK,GAAG,SAAS,OAAO,IAAI,MAAM,EAAE,CAAA;IAC1C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAClE,IAAI,CAAC,GAAG;QAAE,OAAO,iBAAiB,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAEhE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;IAC7B,GAAG,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;IAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IACjC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAA;IACpD,OAAO,iBAAiB,CAAC,GAAG,CAAC,IAAI,EAAE;QAClC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;QACzB,OAAO;KACP,CAAC,CAAA;AACH,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
1
  {
2
- "name": "@growth-labs/video",
3
- "version": "0.3.13",
4
- "type": "module",
5
- "exports": {
6
- ".": {
7
- "types": "./dist/index.d.ts",
8
- "import": "./dist/index.js"
9
- },
10
- "./components": {
11
- "types": "./src/components/index.ts",
12
- "import": "./src/components/index.ts"
13
- },
14
- "./components/*": {
15
- "types": "./src/components/*.astro",
16
- "import": "./src/components/*.astro"
17
- },
18
- "./utils": {
19
- "types": "./dist/utils/index.d.ts",
20
- "import": "./dist/utils/index.js"
21
- },
22
- "./utils/*": {
23
- "types": "./dist/utils/*.d.ts",
24
- "import": "./dist/utils/*.js"
25
- },
26
- "./routes/*": {
27
- "types": "./dist/routes/*.d.ts",
28
- "import": "./dist/routes/*.js",
29
- "default": "./dist/routes/*.js"
30
- }
31
- },
32
- "files": [
33
- "dist",
34
- "src/**/*.ts",
35
- "src/**/*.astro",
36
- "src/**/*.css",
37
- "src/**/*.md",
38
- "src/**/*.sql"
39
- ],
40
- "publishConfig": {
41
- "access": "public",
42
- "registry": "https://registry.npmjs.org/"
43
- },
44
- "scripts": {
45
- "build": "tsc",
46
- "test": "vitest run",
47
- "test:watch": "vitest"
48
- },
49
- "peerDependencies": {
50
- "astro": "^6.1.10",
51
- "vidstack": "^1.12.0"
52
- },
53
- "peerDependenciesMeta": {
54
- "vidstack": {
55
- "optional": true
56
- }
57
- },
58
- "dependencies": {
59
- "zod": "^3.23.0"
60
- },
61
- "devDependencies": {
62
- "@cloudflare/workers-types": "^4.20260402.1",
63
- "astro": "^6.1.10",
64
- "typescript": "^5.7.0",
65
- "vite": "^7.3.2",
66
- "vitest": "^3.0.0",
67
- "vidstack": "^1.12.0"
68
- }
69
- }
2
+ "name": "@growth-labs/video",
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ },
10
+ "./components": {
11
+ "types": "./src/components/index.ts",
12
+ "import": "./src/components/index.ts"
13
+ },
14
+ "./components/*": {
15
+ "types": "./src/components/*.astro",
16
+ "import": "./src/components/*.astro"
17
+ },
18
+ "./utils": {
19
+ "types": "./dist/utils/index.d.ts",
20
+ "import": "./dist/utils/index.js"
21
+ },
22
+ "./utils/*": {
23
+ "types": "./dist/utils/*.d.ts",
24
+ "import": "./dist/utils/*.js"
25
+ },
26
+ "./routes/*": {
27
+ "types": "./dist/routes/*.d.ts",
28
+ "import": "./dist/routes/*.js",
29
+ "default": "./dist/routes/*.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src/**/*.ts",
35
+ "src/**/*.astro",
36
+ "src/**/*.css",
37
+ "src/**/*.md",
38
+ "src/**/*.sql"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "peerDependencies": {
45
+ "astro": "^6.1.10",
46
+ "vidstack": "^1.12.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "vidstack": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "dependencies": {
54
+ "zod": "^3.23.0"
55
+ },
56
+ "devDependencies": {
57
+ "@cloudflare/workers-types": "^4.20260402.1",
58
+ "astro": "^6.4.6",
59
+ "typescript": "^5.7.0",
60
+ "vidstack": "^1.12.0",
61
+ "vite": "^7.3.5",
62
+ "vitest": "^4.1.8"
63
+ },
64
+ "scripts": {
65
+ "build": "tsc",
66
+ "test": "vitest run",
67
+ "test:watch": "vitest"
68
+ }
69
+ }
@@ -177,6 +177,35 @@ const playerBoolAttrs = buildPlayerBooleanAttrs({
177
177
  }
178
178
  })
179
179
  })
180
+
181
+ // `autopictureinpicture` is an HTML attribute on the native <video>
182
+ // element (Apple/WICG; iOS Safari 13.4+). When set on a playing video,
183
+ // iOS auto-enters native PiP whenever the page is hidden — lock screen,
184
+ // tab switch, app switch — so audio + video continue from the system
185
+ // PiP overlay instead of pausing.
186
+ //
187
+ // Vidstack's <media-player> wrapper does not know about this attribute;
188
+ // `vidstack-html.js`'s only setAttribute-on-media call is for
189
+ // `playsinline`. The attribute on the wrapper therefore never reaches
190
+ // the inner <video> that iOS actually inspects, and the feature stays
191
+ // inert. We close the gap by propagating the attribute ourselves: when
192
+ // Vidstack mounts the inner <video> (asynchronously, after provider
193
+ // attach), copy `autopictureinpicture` to it.
194
+ for (const player of document.querySelectorAll<HTMLElement>('media-player[autopictureinpicture]')) {
195
+ const propagate = () => {
196
+ const v = player.querySelector('video')
197
+ if (v && !v.hasAttribute('autopictureinpicture')) {
198
+ v.setAttribute('autopictureinpicture', '')
199
+ return true
200
+ }
201
+ return false
202
+ }
203
+ if (propagate()) continue
204
+ const obs = new MutationObserver(() => {
205
+ if (propagate()) obs.disconnect()
206
+ })
207
+ obs.observe(player, { childList: true, subtree: true })
208
+ }
180
209
  </script>
181
210
 
182
211
  <style>
package/src/options.ts CHANGED
@@ -15,105 +15,127 @@ function parseDurationToSeconds(input: string): number | null {
15
15
  return hours * 3600 + minutes * 60 + seconds
16
16
  }
17
17
 
18
- export const videoOptionsSchema = z.object({
19
- // ─── Required ───
20
- publicDomain: z.string(),
18
+ export const videoOptionsSchema = z
19
+ .object({
20
+ // ─── Required ───
21
+ publicDomain: z.string(),
21
22
 
22
- // ─── R2 bindings ───
23
- r2Binding: z.string().default('MEDIA_BUCKET'),
23
+ // ─── R2 bindings ───
24
+ r2Binding: z.string().default('MEDIA_BUCKET'),
24
25
 
25
- // ─── Admin import/upload helper route (optional) ───
26
- admin: z
27
- .object({
28
- importRoute: z
29
- .object({
30
- enabled: z.boolean().default(false),
31
- path: z.string().default('/api/video/import'),
32
- requireAuth: z.boolean().default(true),
33
- })
34
- .default({}),
35
- })
36
- .default({}),
26
+ // ─── Admin import/upload helper route (optional) ───
27
+ admin: z
28
+ .object({
29
+ importRoute: z
30
+ .object({
31
+ enabled: z.boolean().default(false),
32
+ path: z.string().default('/api/video/import'),
33
+ requireAuth: z.boolean().default(true),
34
+ })
35
+ .default({}),
36
+ })
37
+ .default({}),
37
38
 
38
- // ─── Premium gating (optional) ───
39
- premium: z
40
- .object({
41
- enabled: z.boolean().default(false),
42
- sessionRoute: z.string().default('/api/video/session'),
43
- playbackRoute: z.string().default('/api/video/playback'),
44
- sessionCookieName: z
45
- .string()
46
- .regex(/^[a-zA-Z0-9_-]+$/, 'cookie name must be alphanumeric with underscores or hyphens')
47
- .default('gl_video_session'),
48
- sessionScope: z.enum(['per-video', 'wildcard']).default('per-video'),
49
- sessionTtl: z
50
- .string()
51
- .default('1h')
52
- .transform((value, ctx) => {
53
- const seconds = parseDurationToSeconds(value)
54
- if (seconds === null) {
55
- ctx.addIssue({
56
- code: z.ZodIssueCode.custom,
57
- message: `invalid sessionTtl "${value}". Expected a duration string like "1h", "30m", or "2h30m".`,
58
- })
59
- return z.NEVER
60
- }
61
- if (seconds < SESSION_TTL_MIN_SECONDS) {
62
- ctx.addIssue({
63
- code: z.ZodIssueCode.custom,
64
- message: `sessionTtl "${value}" resolves to ${seconds}s; minimum is ${SESSION_TTL_MIN_SECONDS}s.`,
65
- })
66
- return z.NEVER
67
- }
68
- if (seconds > SESSION_TTL_MAX_SECONDS) {
69
- ctx.addIssue({
70
- code: z.ZodIssueCode.custom,
71
- message: `sessionTtl "${value}" resolves to ${seconds}s; maximum is ${SESSION_TTL_MAX_SECONDS}s (24h).`,
72
- })
73
- return z.NEVER
74
- }
75
- return seconds
76
- }),
77
- signingSecretEnv: z.string().default('GL_VIDEO_SESSION_SECRET'),
78
- accessCheckModule: z
79
- .object({
80
- module: z.string().min(1),
81
- export: z.string().min(1).default('accessCheck'),
82
- })
83
- .optional(),
84
- })
85
- .optional(),
39
+ // ─── Premium gating (optional) ───
40
+ premium: z
41
+ .object({
42
+ enabled: z.boolean().default(false),
43
+ r2Binding: z.string().min(1).optional(),
44
+ sessionRoute: z.string().default('/api/video/session'),
45
+ playbackRoute: z.string().default('/api/video/playback'),
46
+ sessionCookieName: z
47
+ .string()
48
+ .regex(/^[a-zA-Z0-9_-]+$/, 'cookie name must be alphanumeric with underscores or hyphens')
49
+ .default('gl_video_session'),
50
+ sessionScope: z.enum(['per-video', 'wildcard']).default('per-video'),
51
+ sessionTtl: z
52
+ .string()
53
+ .default('1h')
54
+ .transform((value, ctx) => {
55
+ const seconds = parseDurationToSeconds(value)
56
+ if (seconds === null) {
57
+ ctx.addIssue({
58
+ code: z.ZodIssueCode.custom,
59
+ message: `invalid sessionTtl "${value}". Expected a duration string like "1h", "30m", or "2h30m".`,
60
+ })
61
+ return z.NEVER
62
+ }
63
+ if (seconds < SESSION_TTL_MIN_SECONDS) {
64
+ ctx.addIssue({
65
+ code: z.ZodIssueCode.custom,
66
+ message: `sessionTtl "${value}" resolves to ${seconds}s; minimum is ${SESSION_TTL_MIN_SECONDS}s.`,
67
+ })
68
+ return z.NEVER
69
+ }
70
+ if (seconds > SESSION_TTL_MAX_SECONDS) {
71
+ ctx.addIssue({
72
+ code: z.ZodIssueCode.custom,
73
+ message: `sessionTtl "${value}" resolves to ${seconds}s; maximum is ${SESSION_TTL_MAX_SECONDS}s (24h).`,
74
+ })
75
+ return z.NEVER
76
+ }
77
+ return seconds
78
+ }),
79
+ signingSecretEnv: z.string().default('GL_VIDEO_SESSION_SECRET'),
80
+ accessCheckModule: z
81
+ .object({
82
+ module: z.string().min(1),
83
+ export: z.string().min(1).default('accessCheck'),
84
+ })
85
+ .optional(),
86
+ })
87
+ .optional(),
86
88
 
87
- // ─── Player defaults ───
88
- player: z
89
- .object({
90
- autoplay: z.boolean().default(false),
91
- muted: z.boolean().default(false),
92
- preload: z.enum(['none', 'metadata', 'auto']).default('metadata'),
93
- showCaptions: z.boolean().default(true),
94
- showChapters: z.boolean().default(true),
95
- controls: z.boolean().default(true),
96
- startingQuality: z.enum(['auto', 'highest', 'lowest']).default('auto'),
97
- })
98
- .default({}),
89
+ // ─── Player defaults ───
90
+ player: z
91
+ .object({
92
+ autoplay: z.boolean().default(false),
93
+ muted: z.boolean().default(false),
94
+ preload: z.enum(['none', 'metadata', 'auto']).default('metadata'),
95
+ showCaptions: z.boolean().default(true),
96
+ showChapters: z.boolean().default(true),
97
+ controls: z.boolean().default(true),
98
+ startingQuality: z.enum(['auto', 'highest', 'lowest']).default('auto'),
99
+ })
100
+ .default({}),
99
101
 
100
- // ─── YouTube embed fallback ───
101
- youtube: z
102
- .object({
103
- enabled: z.boolean().default(true),
104
- privacyMode: z.boolean().default(true),
105
- lazyLoad: z.boolean().default(true),
106
- })
107
- .default({}),
102
+ // ─── YouTube embed fallback ───
103
+ youtube: z
104
+ .object({
105
+ enabled: z.boolean().default(true),
106
+ privacyMode: z.boolean().default(true),
107
+ lazyLoad: z.boolean().default(true),
108
+ })
109
+ .default({}),
108
110
 
109
- // ─── Analytics ───
110
- analytics: z
111
- .object({
112
- enabled: z.boolean().default(true),
113
- progressMarks: z.array(z.number().min(0).max(1)).default([0.25, 0.5, 0.75, 0.95]),
114
- })
115
- .default({}),
116
- })
111
+ // ─── Analytics ───
112
+ analytics: z
113
+ .object({
114
+ enabled: z.boolean().default(true),
115
+ progressMarks: z.array(z.number().min(0).max(1)).default([0.25, 0.5, 0.75, 0.95]),
116
+ })
117
+ .default({}),
118
+ })
119
+ .superRefine((options, ctx) => {
120
+ if (!options.premium?.enabled) return
121
+ if (!options.premium.r2Binding) {
122
+ ctx.addIssue({
123
+ code: z.ZodIssueCode.custom,
124
+ path: ['premium', 'r2Binding'],
125
+ message:
126
+ 'premium.r2Binding is required when premium.enabled=true and must name a private R2 binding.',
127
+ })
128
+ return
129
+ }
130
+ if (options.premium.r2Binding === options.r2Binding) {
131
+ ctx.addIssue({
132
+ code: z.ZodIssueCode.custom,
133
+ path: ['premium', 'r2Binding'],
134
+ message:
135
+ 'premium.r2Binding must be a distinct private R2 binding; the public r2Binding cannot protect premium media.',
136
+ })
137
+ }
138
+ })
117
139
 
118
140
  export type VideoOptions = z.input<typeof videoOptionsSchema>
119
141
  export type ResolvedVideoOptions = z.output<typeof videoOptionsSchema>
@@ -47,54 +47,69 @@ function readCookie(header: string | null, name: string): string | null {
47
47
  return match ? match[1] : null
48
48
  }
49
49
 
50
+ function protectedResponse(body: BodyInit | null, init: ResponseInit): Response {
51
+ const headers = new Headers(init.headers)
52
+ headers.set('cache-control', 'private, no-store')
53
+ const vary = new Set(
54
+ (headers.get('vary') ?? '')
55
+ .split(',')
56
+ .map((value) => value.trim())
57
+ .filter(Boolean),
58
+ )
59
+ vary.add('Cookie')
60
+ headers.set('vary', [...vary].join(', '))
61
+ return new Response(body, { ...init, headers })
62
+ }
63
+
50
64
  export const GET: APIRoute = async (context) => {
51
65
  const { request, params } = context
52
66
  const env = cloudflareEnv as Record<string, unknown>
53
67
 
54
68
  const rest = params.path as string | undefined
55
69
  if (!rest || rest.length === 0) {
56
- return new Response('invalid path', { status: 400 })
70
+ return protectedResponse('invalid path', { status: 400 })
57
71
  }
58
72
 
59
73
  const segments = rest.split('/').filter(Boolean)
60
74
  if (segments.length < 2) {
61
- return new Response('invalid path', { status: 400 })
75
+ return protectedResponse('invalid path', { status: 400 })
62
76
  }
63
77
  if (segments.some((s) => s === '..' || s === '.')) {
64
- return new Response('invalid path', { status: 400 })
78
+ return protectedResponse('invalid path', { status: 400 })
65
79
  }
66
80
 
67
- if (!config.premium) return new Response('premium disabled', { status: 404 })
81
+ if (!config.premium) return protectedResponse('premium disabled', { status: 404 })
68
82
 
69
83
  const videoId = segments[0]
70
84
  const subKey = segments.slice(1).join('/')
71
85
 
72
86
  const cookie = readCookie(request.headers.get('cookie'), config.premium.sessionCookieName)
73
- if (!cookie) return new Response('unauthorized', { status: 401 })
87
+ if (!cookie) return protectedResponse('forbidden', { status: 403 })
74
88
 
75
89
  const secret = env[config.premium.signingSecretEnv] as string | undefined
76
- if (!secret) return new Response('misconfigured', { status: 500 })
90
+ if (!secret) return protectedResponse('misconfigured', { status: 500 })
77
91
 
78
92
  const session = await verifySession(cookie, secret, videoId)
79
- if (!session) return new Response('forbidden', { status: 403 })
93
+ if (!session) return protectedResponse('forbidden', { status: 403 })
80
94
 
81
- const bucket = env[config.r2Binding] as R2LikeBucket | undefined
82
- if (!bucket) return new Response('misconfigured', { status: 500 })
95
+ const premiumR2Binding = config.premium.r2Binding
96
+ if (!premiumR2Binding) return protectedResponse('misconfigured', { status: 500 })
97
+
98
+ const bucket = env[premiumR2Binding] as R2LikeBucket | undefined
99
+ if (!bucket) return protectedResponse('misconfigured', { status: 500 })
83
100
 
84
101
  const rangeHeader = request.headers.get('range')
85
102
  const range = rangeHeader ? parseRangeHeader(rangeHeader) : undefined
86
103
 
87
104
  const r2Key = `video/${videoId}/${subKey}`
88
105
  const obj = await bucket.get(r2Key, range ? { range } : undefined)
89
- if (!obj) return new Response('not found', { status: 404 })
106
+ if (!obj) return protectedResponse('not found', { status: 404 })
90
107
 
91
108
  const headers = new Headers()
92
109
  obj.writeHttpMetadata(headers)
93
110
  headers.set('etag', obj.httpEtag)
94
111
  headers.set('content-type', inferContentType(r2Key))
95
- headers.set('cache-control', r2Key.endsWith('.m3u8') ? 'no-cache' : 'public, max-age=3600')
96
-
97
- return new Response(obj.body, {
112
+ return protectedResponse(obj.body, {
98
113
  status: range ? 206 : 200,
99
114
  headers,
100
115
  })