zerobucket 0.0.0-stage → 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.
@@ -0,0 +1,64 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { Z as ZeroBucket } from './client-WZ4VRCPi.cjs';
3
+ import 'pg';
4
+
5
+ /**
6
+ * HTTP serving helpers: `import { createImageHandler } from "zerobucket/http"`.
7
+ *
8
+ * `createImageHandler()` returns a Web-standard `(Request) => Promise<Response>`,
9
+ * which is what Next.js route handlers, Hono, Remix, SvelteKit, Bun, Deno and
10
+ * Cloudflare-style runtimes all speak. `toNodeHandler()` adapts it to
11
+ * Express / Fastify / plain `node:http`.
12
+ *
13
+ * Implements: ETag (the content SHA-256) + If-None-Match -> 304; Range /
14
+ * If-Range -> 206 (single byte range) with 416 for unsatisfiable ranges;
15
+ * HEAD; X-Content-Type-Options: nosniff. Bytes are streamed in chunks, never
16
+ * fully buffered.
17
+ */
18
+
19
+ type ImageHandler = (req: Request, ctx?: {
20
+ id?: string;
21
+ }) => Promise<Response>;
22
+ interface ImageHandlerOptions<C = unknown> {
23
+ /** Extract the id from the request. Default: the last non-empty path segment. Ignored if `ctx.id` is passed. */
24
+ getId?: (req: Request) => string | undefined;
25
+ /** Build the context handed to your beforeGet hook (e.g. the signed-in user from cookies). */
26
+ getContext?: (req: Request) => C | undefined | Promise<C | undefined>;
27
+ /**
28
+ * Cache-Control header. The default, "private, no-cache", is safe for
29
+ * access-controlled images: browsers always revalidate, and the ETag makes
30
+ * that a cheap 304. For public images pass e.g. "public, max-age=31536000, immutable"
31
+ * (ids are never reused and content never mutates).
32
+ */
33
+ cacheControl?: string;
34
+ /** "inline" (default) or "attachment" (adds the stored filename). */
35
+ disposition?: "inline" | "attachment";
36
+ /** Bytes per DB read while streaming. Default 1 MiB. */
37
+ chunkSize?: number;
38
+ /** Called with unexpected errors (anything other than not-found / denied) so you can log them. The response is a generic 500. */
39
+ onError?: (error: unknown, req: Request) => void;
40
+ }
41
+ declare function createImageHandler<C = unknown>(images: ZeroBucket<C>, options?: ImageHandlerOptions<C>): ImageHandler;
42
+ /**
43
+ * Adapt an ImageHandler to Node's `(req, res)` signature (Express, Fastify's raw
44
+ * handler, plain `http.createServer`).
45
+ *
46
+ * app.get("/images/:id", (req, res) => nodeHandler(req, res, { id: req.params.id }));
47
+ */
48
+ declare function toNodeHandler(handler: ImageHandler): (req: IncomingMessage, res: ServerResponse, ctx?: {
49
+ id?: string;
50
+ }) => Promise<void>;
51
+ /** Weak comparison, per RFC 9110 for If-None-Match. */
52
+ declare function etagMatches(header: string | null, etag: string): boolean;
53
+ /**
54
+ * Parse a single `bytes=` range against a resource of `size` bytes.
55
+ * - returns a clamped inclusive range
56
+ * - "unsatisfiable" -> respond 416
57
+ * - null -> ignore the header and serve the full body (syntax we don't handle, incl. multi-range)
58
+ */
59
+ declare function parseRange(header: string, size: number): {
60
+ start: number;
61
+ end: number;
62
+ } | "unsatisfiable" | null;
63
+
64
+ export { type ImageHandler, type ImageHandlerOptions, createImageHandler, etagMatches, parseRange, toNodeHandler };
package/dist/http.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { Z as ZeroBucket } from './client-WZ4VRCPi.js';
3
+ import 'pg';
4
+
5
+ /**
6
+ * HTTP serving helpers: `import { createImageHandler } from "zerobucket/http"`.
7
+ *
8
+ * `createImageHandler()` returns a Web-standard `(Request) => Promise<Response>`,
9
+ * which is what Next.js route handlers, Hono, Remix, SvelteKit, Bun, Deno and
10
+ * Cloudflare-style runtimes all speak. `toNodeHandler()` adapts it to
11
+ * Express / Fastify / plain `node:http`.
12
+ *
13
+ * Implements: ETag (the content SHA-256) + If-None-Match -> 304; Range /
14
+ * If-Range -> 206 (single byte range) with 416 for unsatisfiable ranges;
15
+ * HEAD; X-Content-Type-Options: nosniff. Bytes are streamed in chunks, never
16
+ * fully buffered.
17
+ */
18
+
19
+ type ImageHandler = (req: Request, ctx?: {
20
+ id?: string;
21
+ }) => Promise<Response>;
22
+ interface ImageHandlerOptions<C = unknown> {
23
+ /** Extract the id from the request. Default: the last non-empty path segment. Ignored if `ctx.id` is passed. */
24
+ getId?: (req: Request) => string | undefined;
25
+ /** Build the context handed to your beforeGet hook (e.g. the signed-in user from cookies). */
26
+ getContext?: (req: Request) => C | undefined | Promise<C | undefined>;
27
+ /**
28
+ * Cache-Control header. The default, "private, no-cache", is safe for
29
+ * access-controlled images: browsers always revalidate, and the ETag makes
30
+ * that a cheap 304. For public images pass e.g. "public, max-age=31536000, immutable"
31
+ * (ids are never reused and content never mutates).
32
+ */
33
+ cacheControl?: string;
34
+ /** "inline" (default) or "attachment" (adds the stored filename). */
35
+ disposition?: "inline" | "attachment";
36
+ /** Bytes per DB read while streaming. Default 1 MiB. */
37
+ chunkSize?: number;
38
+ /** Called with unexpected errors (anything other than not-found / denied) so you can log them. The response is a generic 500. */
39
+ onError?: (error: unknown, req: Request) => void;
40
+ }
41
+ declare function createImageHandler<C = unknown>(images: ZeroBucket<C>, options?: ImageHandlerOptions<C>): ImageHandler;
42
+ /**
43
+ * Adapt an ImageHandler to Node's `(req, res)` signature (Express, Fastify's raw
44
+ * handler, plain `http.createServer`).
45
+ *
46
+ * app.get("/images/:id", (req, res) => nodeHandler(req, res, { id: req.params.id }));
47
+ */
48
+ declare function toNodeHandler(handler: ImageHandler): (req: IncomingMessage, res: ServerResponse, ctx?: {
49
+ id?: string;
50
+ }) => Promise<void>;
51
+ /** Weak comparison, per RFC 9110 for If-None-Match. */
52
+ declare function etagMatches(header: string | null, etag: string): boolean;
53
+ /**
54
+ * Parse a single `bytes=` range against a resource of `size` bytes.
55
+ * - returns a clamped inclusive range
56
+ * - "unsatisfiable" -> respond 416
57
+ * - null -> ignore the header and serve the full body (syntax we don't handle, incl. multi-range)
58
+ */
59
+ declare function parseRange(header: string, size: number): {
60
+ start: number;
61
+ end: number;
62
+ } | "unsatisfiable" | null;
63
+
64
+ export { type ImageHandler, type ImageHandlerOptions, createImageHandler, etagMatches, parseRange, toNodeHandler };
package/dist/http.js ADDED
@@ -0,0 +1,149 @@
1
+ import {
2
+ AccessDeniedError,
3
+ ImageNotFoundError
4
+ } from "./chunk-W2C37XYV.js";
5
+
6
+ // src/http.ts
7
+ import { Readable } from "stream";
8
+ function createImageHandler(images, options = {}) {
9
+ const cacheControl = options.cacheControl ?? "private, no-cache";
10
+ return async function handle(req, ctx) {
11
+ if (req.method !== "GET" && req.method !== "HEAD") {
12
+ return new Response("Method Not Allowed", {
13
+ status: 405,
14
+ headers: { Allow: "GET, HEAD" }
15
+ });
16
+ }
17
+ const id = ctx?.id ?? (options.getId ?? defaultGetId)(req);
18
+ if (!id) return notFound();
19
+ try {
20
+ const context = options.getContext ? await options.getContext(req) : void 0;
21
+ const meta = await images.metadata(id, { context });
22
+ const etag = `"${meta.checksumSha256}"`;
23
+ const size = meta.sizeBytes;
24
+ const base = {
25
+ ETag: etag,
26
+ "Cache-Control": cacheControl,
27
+ "Accept-Ranges": "bytes",
28
+ "X-Content-Type-Options": "nosniff",
29
+ "Content-Type": meta.mimeType
30
+ };
31
+ if (options.disposition === "attachment") {
32
+ base["Content-Disposition"] = attachmentHeader(
33
+ meta.filename ?? "download"
34
+ );
35
+ }
36
+ if (etagMatches(req.headers.get("if-none-match"), etag)) {
37
+ return new Response(null, {
38
+ status: 304,
39
+ headers: { ETag: etag, "Cache-Control": cacheControl }
40
+ });
41
+ }
42
+ let range = null;
43
+ const rangeHeader = req.headers.get("range");
44
+ const ifRange = req.headers.get("if-range");
45
+ if (rangeHeader && (ifRange === null || ifRange.trim() === etag)) {
46
+ range = parseRange(rangeHeader, size);
47
+ }
48
+ if (range === "unsatisfiable") {
49
+ return new Response(null, {
50
+ status: 416,
51
+ headers: {
52
+ ...base,
53
+ "Content-Range": `bytes */${size}`,
54
+ "Content-Length": "0"
55
+ }
56
+ });
57
+ }
58
+ const status = range ? 206 : 200;
59
+ const length = range ? range.end - range.start + 1 : size;
60
+ const headers = {
61
+ ...base,
62
+ "Content-Length": String(length)
63
+ };
64
+ if (range)
65
+ headers["Content-Range"] = `bytes ${range.start}-${range.end}/${size}`;
66
+ if (req.method === "HEAD") return new Response(null, { status, headers });
67
+ const body = await images.toWebStream(id, {
68
+ context,
69
+ chunkSize: options.chunkSize,
70
+ range: range ? { start: range.start, end: range.end } : void 0
71
+ });
72
+ return new Response(body, { status, headers });
73
+ } catch (exc) {
74
+ if (exc instanceof ImageNotFoundError) return notFound();
75
+ if (exc instanceof AccessDeniedError)
76
+ return new Response("Forbidden", { status: 403 });
77
+ options.onError?.(exc, req);
78
+ return new Response("Internal Server Error", { status: 500 });
79
+ }
80
+ };
81
+ }
82
+ function toNodeHandler(handler) {
83
+ return async function nodeHandler(req, res, ctx) {
84
+ const host = req.headers.host ?? "localhost";
85
+ const headers = new Headers();
86
+ for (const [k, v] of Object.entries(req.headers)) {
87
+ if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));
88
+ else if (v !== void 0) headers.set(k, v);
89
+ }
90
+ const request = new Request(`http://${host}${req.url ?? "/"}`, {
91
+ method: req.method,
92
+ headers
93
+ });
94
+ const response = await handler(request, ctx);
95
+ res.writeHead(response.status, Object.fromEntries(response.headers));
96
+ if (!response.body) {
97
+ res.end();
98
+ return;
99
+ }
100
+ const nodeStream = Readable.fromWeb(
101
+ response.body
102
+ );
103
+ res.on("close", () => nodeStream.destroy());
104
+ nodeStream.on("error", () => res.destroy());
105
+ nodeStream.pipe(res);
106
+ };
107
+ }
108
+ function notFound() {
109
+ return new Response("Not Found", { status: 404 });
110
+ }
111
+ function defaultGetId(req) {
112
+ const segments = new URL(req.url).pathname.split("/").filter(Boolean);
113
+ const last = segments[segments.length - 1];
114
+ return last ? decodeURIComponent(last) : void 0;
115
+ }
116
+ function etagMatches(header, etag) {
117
+ if (!header) return false;
118
+ if (header.trim() === "*") return true;
119
+ const strip = (t) => t.trim().replace(/^W\//, "");
120
+ return header.split(",").some((t) => strip(t) === etag);
121
+ }
122
+ function parseRange(header, size) {
123
+ const m = /^bytes=(\d*)-(\d*)$/i.exec(header.trim());
124
+ if (!m) return null;
125
+ const [, a, b] = m;
126
+ if (a === "" && b === "") return null;
127
+ if (a === "") {
128
+ const n = Number(b);
129
+ if (n === 0) return "unsatisfiable";
130
+ return { start: Math.max(0, size - n), end: size - 1 };
131
+ }
132
+ const start = Number(a);
133
+ if (start >= size) return "unsatisfiable";
134
+ if (b === "") return { start, end: size - 1 };
135
+ const end = Number(b);
136
+ if (end < start) return null;
137
+ return { start, end: Math.min(end, size - 1) };
138
+ }
139
+ function attachmentHeader(filename) {
140
+ const ascii = filename.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
141
+ return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
142
+ }
143
+ export {
144
+ createImageHandler,
145
+ etagMatches,
146
+ parseRange,
147
+ toNodeHandler
148
+ };
149
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http.ts"],"sourcesContent":["/**\r\n * HTTP serving helpers: `import { createImageHandler } from \"zerobucket/http\"`.\r\n *\r\n * `createImageHandler()` returns a Web-standard `(Request) => Promise<Response>`,\r\n * which is what Next.js route handlers, Hono, Remix, SvelteKit, Bun, Deno and\r\n * Cloudflare-style runtimes all speak. `toNodeHandler()` adapts it to\r\n * Express / Fastify / plain `node:http`.\r\n *\r\n * Implements: ETag (the content SHA-256) + If-None-Match -> 304; Range /\r\n * If-Range -> 206 (single byte range) with 416 for unsatisfiable ranges;\r\n * HEAD; X-Content-Type-Options: nosniff. Bytes are streamed in chunks, never\r\n * fully buffered.\r\n */\r\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\r\nimport { Readable } from \"node:stream\";\r\nimport { AccessDeniedError, ImageNotFoundError } from \"./errors.js\";\r\nimport type { ZeroBucket } from \"./client.js\";\r\n\r\nexport type ImageHandler = (\r\n req: Request,\r\n ctx?: { id?: string },\r\n) => Promise<Response>;\r\n\r\nexport interface ImageHandlerOptions<C = unknown> {\r\n /** Extract the id from the request. Default: the last non-empty path segment. Ignored if `ctx.id` is passed. */\r\n getId?: (req: Request) => string | undefined;\r\n /** Build the context handed to your beforeGet hook (e.g. the signed-in user from cookies). */\r\n getContext?: (req: Request) => C | undefined | Promise<C | undefined>;\r\n /**\r\n * Cache-Control header. The default, \"private, no-cache\", is safe for\r\n * access-controlled images: browsers always revalidate, and the ETag makes\r\n * that a cheap 304. For public images pass e.g. \"public, max-age=31536000, immutable\"\r\n * (ids are never reused and content never mutates).\r\n */\r\n cacheControl?: string;\r\n /** \"inline\" (default) or \"attachment\" (adds the stored filename). */\r\n disposition?: \"inline\" | \"attachment\";\r\n /** Bytes per DB read while streaming. Default 1 MiB. */\r\n chunkSize?: number;\r\n /** Called with unexpected errors (anything other than not-found / denied) so you can log them. The response is a generic 500. */\r\n onError?: (error: unknown, req: Request) => void;\r\n}\r\n\r\nexport function createImageHandler<C = unknown>(\r\n images: ZeroBucket<C>,\r\n options: ImageHandlerOptions<C> = {},\r\n): ImageHandler {\r\n const cacheControl = options.cacheControl ?? \"private, no-cache\";\r\n\r\n return async function handle(req, ctx) {\r\n if (req.method !== \"GET\" && req.method !== \"HEAD\") {\r\n return new Response(\"Method Not Allowed\", {\r\n status: 405,\r\n headers: { Allow: \"GET, HEAD\" },\r\n });\r\n }\r\n\r\n const id = ctx?.id ?? (options.getId ?? defaultGetId)(req);\r\n if (!id) return notFound();\r\n\r\n try {\r\n const context = options.getContext\r\n ? await options.getContext(req)\r\n : undefined;\r\n const meta = await images.metadata(id, { context });\r\n const etag = `\"${meta.checksumSha256}\"`;\r\n const size = meta.sizeBytes;\r\n\r\n const base: Record<string, string> = {\r\n ETag: etag,\r\n \"Cache-Control\": cacheControl,\r\n \"Accept-Ranges\": \"bytes\",\r\n \"X-Content-Type-Options\": \"nosniff\",\r\n \"Content-Type\": meta.mimeType,\r\n };\r\n if (options.disposition === \"attachment\") {\r\n base[\"Content-Disposition\"] = attachmentHeader(\r\n meta.filename ?? \"download\",\r\n );\r\n }\r\n\r\n if (etagMatches(req.headers.get(\"if-none-match\"), etag)) {\r\n return new Response(null, {\r\n status: 304,\r\n headers: { ETag: etag, \"Cache-Control\": cacheControl },\r\n });\r\n }\r\n\r\n // Range is honoured unless If-Range is present and does not match our (strong) ETag.\r\n let range: ReturnType<typeof parseRange> = null;\r\n const rangeHeader = req.headers.get(\"range\");\r\n const ifRange = req.headers.get(\"if-range\");\r\n if (rangeHeader && (ifRange === null || ifRange.trim() === etag)) {\r\n range = parseRange(rangeHeader, size);\r\n }\r\n\r\n if (range === \"unsatisfiable\") {\r\n return new Response(null, {\r\n status: 416,\r\n headers: {\r\n ...base,\r\n \"Content-Range\": `bytes */${size}`,\r\n \"Content-Length\": \"0\",\r\n },\r\n });\r\n }\r\n\r\n const status = range ? 206 : 200;\r\n const length = range ? range.end - range.start + 1 : size;\r\n const headers: Record<string, string> = {\r\n ...base,\r\n \"Content-Length\": String(length),\r\n };\r\n if (range)\r\n headers[\"Content-Range\"] = `bytes ${range.start}-${range.end}/${size}`;\r\n\r\n if (req.method === \"HEAD\") return new Response(null, { status, headers });\r\n\r\n const body = await images.toWebStream(id, {\r\n context,\r\n chunkSize: options.chunkSize,\r\n range: range ? { start: range.start, end: range.end } : undefined,\r\n });\r\n return new Response(body, { status, headers });\r\n } catch (exc) {\r\n if (exc instanceof ImageNotFoundError) return notFound();\r\n if (exc instanceof AccessDeniedError)\r\n return new Response(\"Forbidden\", { status: 403 });\r\n options.onError?.(exc, req);\r\n return new Response(\"Internal Server Error\", { status: 500 });\r\n }\r\n };\r\n}\r\n\r\n// ---- Node adapter --------------------------------------------------------\r\n\r\n/**\r\n * Adapt an ImageHandler to Node's `(req, res)` signature (Express, Fastify's raw\r\n * handler, plain `http.createServer`).\r\n *\r\n * app.get(\"/images/:id\", (req, res) => nodeHandler(req, res, { id: req.params.id }));\r\n */\r\nexport function toNodeHandler(handler: ImageHandler) {\r\n return async function nodeHandler(\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ctx?: { id?: string },\r\n ): Promise<void> {\r\n const host = req.headers.host ?? \"localhost\";\r\n const headers = new Headers();\r\n for (const [k, v] of Object.entries(req.headers)) {\r\n if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));\r\n else if (v !== undefined) headers.set(k, v);\r\n }\r\n const request = new Request(`http://${host}${req.url ?? \"/\"}`, {\r\n method: req.method,\r\n headers,\r\n });\r\n const response = await handler(request, ctx);\r\n\r\n res.writeHead(response.status, Object.fromEntries(response.headers));\r\n if (!response.body) {\r\n res.end();\r\n return;\r\n }\r\n const nodeStream = Readable.fromWeb(\r\n response.body as import(\"node:stream/web\").ReadableStream,\r\n );\r\n // If the client disconnects, stop reading from the database.\r\n res.on(\"close\", () => nodeStream.destroy());\r\n nodeStream.on(\"error\", () => res.destroy());\r\n nodeStream.pipe(res);\r\n };\r\n}\r\n\r\n// ---- helpers (exported for tests) ----------------------------------------\r\n\r\nfunction notFound(): Response {\r\n return new Response(\"Not Found\", { status: 404 });\r\n}\r\n\r\nfunction defaultGetId(req: Request): string | undefined {\r\n const segments = new URL(req.url).pathname.split(\"/\").filter(Boolean);\r\n const last = segments[segments.length - 1];\r\n return last ? decodeURIComponent(last) : undefined;\r\n}\r\n\r\n/** Weak comparison, per RFC 9110 for If-None-Match. */\r\nexport function etagMatches(header: string | null, etag: string): boolean {\r\n if (!header) return false;\r\n if (header.trim() === \"*\") return true;\r\n const strip = (t: string) => t.trim().replace(/^W\\//, \"\");\r\n return header.split(\",\").some((t) => strip(t) === etag);\r\n}\r\n\r\n/**\r\n * Parse a single `bytes=` range against a resource of `size` bytes.\r\n * - returns a clamped inclusive range\r\n * - \"unsatisfiable\" -> respond 416\r\n * - null -> ignore the header and serve the full body (syntax we don't handle, incl. multi-range)\r\n */\r\nexport function parseRange(\r\n header: string,\r\n size: number,\r\n): { start: number; end: number } | \"unsatisfiable\" | null {\r\n const m = /^bytes=(\\d*)-(\\d*)$/i.exec(header.trim());\r\n if (!m) return null; // also rejects multi-range (\"bytes=0-1,5-6\")\r\n const [, a, b] = m as unknown as [string, string, string];\r\n if (a === \"\" && b === \"\") return null;\r\n\r\n if (a === \"\") {\r\n const n = Number(b); // suffix: last n bytes\r\n if (n === 0) return \"unsatisfiable\";\r\n return { start: Math.max(0, size - n), end: size - 1 };\r\n }\r\n const start = Number(a);\r\n if (start >= size) return \"unsatisfiable\";\r\n if (b === \"\") return { start, end: size - 1 };\r\n const end = Number(b);\r\n if (end < start) return null; // syntactically invalid -> ignore\r\n return { start, end: Math.min(end, size - 1) };\r\n}\r\n\r\nfunction attachmentHeader(filename: string): string {\r\n const ascii = filename.replace(/[^\\x20-\\x7e]/g, \"_\").replace(/[\"\\\\]/g, \"_\");\r\n return `attachment; filename=\"${ascii}\"; filename*=UTF-8''${encodeURIComponent(filename)}`;\r\n}\r\n"],"mappings":"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","names":[]}