Skip to content

Commit c9dc192

Browse files
committed
Serve AI models from Cloudflare R2 via Worker binding
- hooks.server.ts intercepts /models/* requests and serves from R2 - Removed prebuild model fetch from apps/web/package.json - Models uploaded to shieldcv-models R2 bucket (130 MB) - Browser sees same-origin /models/ requests, CSP stays clean - Cache-control: immutable for permanent browser caching - No model files in build output, bypasses Pages 25 MB limit
1 parent ea3195e commit c9dc192

2 files changed

Lines changed: 31 additions & 6 deletions

File tree

apps/web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite dev",
8-
"prebuild": "node ../../scripts/fetch-models.mjs",
98
"build": "vite build",
109
"preview": "vite preview",
1110
"lint": "eslint . --ext .js,.ts,.svelte",

apps/web/src/hooks.server.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
11
import type { Handle } from '@sveltejs/kit';
22
import { getSecurityHeaders } from '$lib/security';
33

4-
function extractCspNonce(response: Response): string | undefined {
5-
return response.headers.get('content-security-policy')?.match(/'nonce-([^']+)'/)?.[1];
4+
const CONTENT_TYPES: Record<string, string> = {
5+
'.json': 'application/json',
6+
'.onnx': 'application/octet-stream',
7+
'.wasm': 'application/wasm',
8+
};
9+
10+
function getContentType(path: string): string {
11+
const ext = path.slice(path.lastIndexOf('.'));
12+
return CONTENT_TYPES[ext] || 'application/octet-stream';
613
}
714

815
export const handle: Handle = async ({ event, resolve }) => {
9-
const response = await resolve(event);
10-
const nonce = extractCspNonce(response);
16+
// Serve AI model files from R2 bucket
17+
if (event.url.pathname.startsWith('/models/')) {
18+
const key = event.url.pathname.slice(1); // strip leading slash
19+
const bucket = /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
20+
(event.platform as any)?.env?.MODELS;
21+
if (!bucket) {
22+
return new Response('Model storage not configured', { status: 503 });
23+
}
24+
const object = await bucket.get(key);
25+
if (!object) {
26+
return new Response('Model not found', { status: 404 });
27+
}
28+
return new Response(object.body, {
29+
headers: {
30+
'content-type': getContentType(key),
31+
'cache-control': 'public, max-age=31536000, immutable',
32+
'access-control-allow-origin': '*',
33+
},
34+
});
35+
}
1136

37+
const response = await resolve(event);
38+
const nonce = response.headers.get('content-security-policy')?.match(/'nonce-([^']+)'/)?.[1];
1239
for (const [header, value] of Object.entries(getSecurityHeaders(event.url.pathname, { nonce }))) {
1340
response.headers.set(header, value);
1441
}
15-
1642
return response;
1743
};

0 commit comments

Comments
 (0)