@growth-labs/video 0.3.14 → 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 +56 -3
- package/dist/options.d.ts +90 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +22 -1
- package/dist/options.js.map +1 -1
- package/dist/routes/playback.d.ts.map +1 -1
- package/dist/routes/playback.js +25 -12
- package/dist/routes/playback.js.map +1 -1
- package/package.json +68 -68
- package/src/options.ts +115 -93
- package/src/routes/playback.ts +28 -13
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
|
|
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;
|
package/dist/options.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
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
|
package/dist/options.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,uBAAuB,GAAG,EAAE,CAAA;AAClC,MAAM,uBAAuB,GAAG,MAAM,CAAA;AAEtC,SAAS,sBAAsB,CAAC,KAAa;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAChE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAA;IAChE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAA;IACzB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACtE,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5C,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9C,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9C,OAAO,KAAK,GAAG,IAAI,GAAG,OAAO,GAAG,EAAE,GAAG,OAAO,CAAA;AAC7C,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,
|
|
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;
|
|
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"}
|
package/dist/routes/playback.js
CHANGED
|
@@ -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
|
|
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
|
|
58
|
+
return protectedResponse('invalid path', { status: 400 });
|
|
48
59
|
}
|
|
49
60
|
if (segments.some((s) => s === '..' || s === '.')) {
|
|
50
|
-
return
|
|
61
|
+
return protectedResponse('invalid path', { status: 400 });
|
|
51
62
|
}
|
|
52
63
|
if (!config.premium)
|
|
53
|
-
return
|
|
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
|
|
69
|
+
return protectedResponse('forbidden', { status: 403 });
|
|
59
70
|
const secret = env[config.premium.signingSecretEnv];
|
|
60
71
|
if (!secret)
|
|
61
|
-
return
|
|
72
|
+
return protectedResponse('misconfigured', { status: 500 });
|
|
62
73
|
const session = await verifySession(cookie, secret, videoId);
|
|
63
74
|
if (!session)
|
|
64
|
-
return
|
|
65
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
1
|
+
{"version":3,"file":"playback.js","sourceRoot":"","sources":["../../src/routes/playback.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
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
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export const videoOptionsSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
// ─── Required ───
|
|
21
|
+
publicDomain: z.string(),
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
// ─── R2 bindings ───
|
|
24
|
+
r2Binding: z.string().default('MEDIA_BUCKET'),
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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>
|
package/src/routes/playback.ts
CHANGED
|
@@ -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
|
|
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
|
|
75
|
+
return protectedResponse('invalid path', { status: 400 })
|
|
62
76
|
}
|
|
63
77
|
if (segments.some((s) => s === '..' || s === '.')) {
|
|
64
|
-
return
|
|
78
|
+
return protectedResponse('invalid path', { status: 400 })
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
if (!config.premium) return
|
|
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
|
|
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
|
|
90
|
+
if (!secret) return protectedResponse('misconfigured', { status: 500 })
|
|
77
91
|
|
|
78
92
|
const session = await verifySession(cookie, secret, videoId)
|
|
79
|
-
if (!session) return
|
|
93
|
+
if (!session) return protectedResponse('forbidden', { status: 403 })
|
|
80
94
|
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
return new Response(obj.body, {
|
|
112
|
+
return protectedResponse(obj.body, {
|
|
98
113
|
status: range ? 206 : 200,
|
|
99
114
|
headers,
|
|
100
115
|
})
|