zerobucket 0.1.0 → 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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/adapters/postgres.ts","../src/validation.ts","../src/optimize.ts","../src/validators/pdf.ts"],"sourcesContent":["/**\r\n * The public ZeroBucket SDK entry point.\r\n *\r\n * const images = new ZeroBucket({ connectionString: process.env.DATABASE_URL });\r\n * const id = await images.put(fileBuffer);\r\n * const image = await images.get(id);\r\n *\r\n * The developer never thinks about BYTEA, checksums or connection pooling.\r\n */\r\nimport { createHash } from \"node:crypto\";\r\nimport { readFile, stat } from \"node:fs/promises\";\r\nimport { basename } from \"node:path\";\r\nimport type { Pool } from \"pg\";\r\nimport type { CallOptions, StorageBackend } from \"./adapters/base.js\";\r\nimport {\r\n DEFAULT_STREAM_CHUNK_SIZE,\r\n PostgresBackend,\r\n} from \"./adapters/postgres.js\";\r\nimport {\r\n AccessDeniedError,\r\n ImageNotFoundError,\r\n ImageTooLargeError,\r\n ImageValidationError,\r\n} from \"./errors.js\";\r\nimport { type OptimizeOptions, optimizeImage } from \"./optimize.js\";\r\nimport type {\r\n BatchDeleteResult,\r\n BatchGetResult,\r\n BatchPutResult,\r\n Image,\r\n ImageMetadata,\r\n OperationEvent,\r\n PreparedRow,\r\n Queryable,\r\n} from \"./types.js\";\r\nimport {\r\n DEFAULT_MAX_PIXELS,\r\n SUPPORTED_FORMATS,\r\n validateImage,\r\n} from \"./validation.js\";\r\nimport type { ContentValidator } from \"./validators/types.js\";\r\n\r\n/** 8 MiB: a practical ceiling for \"small app\" images. Override with `maxBytes`. */\r\nexport const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;\r\n\r\n/**\r\n * What put() accepts:\r\n * - Buffer / Uint8Array / ArrayBuffer: the bytes themselves\r\n * - string: a filesystem PATH (never raw content)\r\n * - Blob / File (e.g. from `request.formData()`)\r\n * - Node Readable or web ReadableStream (read in bounded chunks)\r\n */\r\nexport type ImageInput =\r\n | Buffer\r\n | Uint8Array\r\n | ArrayBuffer\r\n | string\r\n | Blob\r\n | AsyncIterable<Uint8Array | string>;\r\n\r\nexport type BeforeGetHook<C = unknown> = (\r\n imageId: string,\r\n context: C | undefined,\r\n) => boolean | Promise<boolean>;\r\nexport type BeforePutHook<C = unknown> = (\r\n context: C | undefined,\r\n) => boolean | Promise<boolean>;\r\n\r\nexport interface ZeroBucketOptions<C = unknown> {\r\n /** PostgreSQL connection string. */\r\n connectionString?: string;\r\n /** Bring your own pg.Pool. It is NOT closed by close(). */\r\n pool?: Pool;\r\n /** Advanced: inject a custom StorageBackend instead of building a PostgresBackend. */\r\n backend?: StorageBackend;\r\n\r\n /** Max accepted size in bytes. Default 8 MiB. */\r\n maxBytes?: number;\r\n /** Decoded-pixel ceiling that rejects decompression bombs. Default ~89 MP. */\r\n maxPixels?: number;\r\n /** Accepted image formats. Default [\"jpeg\", \"png\", \"webp\"]. */\r\n allowedFormats?: readonly string[];\r\n\r\n autoMigrate?: boolean;\r\n maxRetries?: number;\r\n retryBaseDelayMs?: number;\r\n poolMinSize?: number;\r\n poolMaxSize?: number;\r\n poolTimeoutMs?: number;\r\n /** Fire-and-forget observability. Exceptions thrown inside it are swallowed. */\r\n onOperation?: (event: OperationEvent) => void;\r\n\r\n /**\r\n * Authorization hook for get / getMany (per id) / getStream / streamTo / metadata.\r\n * Return false to deny. A hook that THROWS fails closed: the exception\r\n * propagates (or is captured per item in getMany) and is never treated as allow.\r\n * NOT called for exists().\r\n */\r\n beforeGet?: BeforeGetHook<C>;\r\n /** Authorization hook for put / putMany (evaluated once per call). Same fail-closed rule. */\r\n beforePut?: BeforePutHook<C>;\r\n}\r\n\r\ninterface PutBase<C> {\r\n /** Display filename stored in metadata (never used for validation). */\r\n filename?: string;\r\n /** `true` = strip metadata only; or pass { maxWidth, format, quality } to resize/re-encode. */\r\n optimize?: boolean | OptimizeOptions;\r\n /** Store non-image content through the same machinery. Incompatible with `optimize`. */\r\n validator?: ContentValidator;\r\n /** Join your own transaction. ZeroBucket then does not commit, roll back or retry. */\r\n connection?: Queryable;\r\n /** Passed to beforePut. */\r\n context?: C;\r\n}\r\nexport type PutOptions<C = unknown> = PutBase<C>;\r\nexport interface PutManyOptions<C = unknown> extends Omit<\r\n PutBase<C>,\r\n \"filename\"\r\n> {\r\n /** Same length as `images`; use null/undefined entries to fall back to each input's own name. */\r\n filenames?: (string | null | undefined)[];\r\n}\r\nexport interface GetOptions<C = unknown> {\r\n connection?: Queryable;\r\n context?: C;\r\n}\r\nexport interface StreamOptions<C = unknown> extends GetOptions<C> {\r\n /** Bytes per chunk. Default 1 MiB. */\r\n chunkSize?: number;\r\n /** Inclusive byte range, e.g. { start: 0, end: 1023 }. `end` is clamped to the object size. */\r\n range?: { start: number; end?: number };\r\n}\r\n\r\n/** Anything with write(); e.g. fs.WriteStream, http.ServerResponse, or a custom sink. Backpressure is honoured. */\r\nexport interface WritableLike {\r\n write(chunk: Buffer): boolean | void | Promise<void>;\r\n once?(event: \"drain\", listener: () => void): unknown;\r\n}\r\n\r\n/** Bounded concurrency for per-item validation work. */\r\nconst PREPARE_CONCURRENCY = 4;\r\n\r\nexport class ZeroBucket<C = unknown> {\r\n private readonly backend: StorageBackend;\r\n private readonly maxBytes: number;\r\n private readonly maxPixels: number;\r\n private readonly allowedFormats: readonly string[];\r\n private readonly beforeGet: BeforeGetHook<C> | undefined;\r\n private readonly beforePut: BeforePutHook<C> | undefined;\r\n\r\n constructor(options: ZeroBucketOptions<C> = {}) {\r\n this.backend =\r\n options.backend ??\r\n new PostgresBackend({\r\n connectionString: options.connectionString,\r\n pool: options.pool,\r\n autoMigrate: options.autoMigrate,\r\n maxRetries: options.maxRetries,\r\n retryBaseDelayMs: options.retryBaseDelayMs,\r\n poolMinSize: options.poolMinSize,\r\n poolMaxSize: options.poolMaxSize,\r\n poolTimeoutMs: options.poolTimeoutMs,\r\n onOperation: options.onOperation,\r\n });\r\n this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\r\n this.maxPixels = options.maxPixels ?? DEFAULT_MAX_PIXELS;\r\n this.allowedFormats = options.allowedFormats ?? SUPPORTED_FORMATS;\r\n this.beforeGet = options.beforeGet;\r\n this.beforePut = options.beforePut;\r\n }\r\n\r\n /** Construct AND connect/migrate immediately, so a bad DATABASE_URL fails at startup, not on first request. */\r\n static async connect<C = unknown>(\r\n options: ZeroBucketOptions<C> = {},\r\n ): Promise<ZeroBucket<C>> {\r\n const zb = new ZeroBucket<C>(options);\r\n await zb.ready();\r\n return zb;\r\n }\r\n\r\n /** Resolves once connected and migrated. Called automatically on first use. */\r\n ready(): Promise<void> {\r\n return this.backend.ready();\r\n }\r\n\r\n // ---- hooks -----------------------------------------------------------\r\n\r\n private async checkBeforeGet(\r\n imageId: string,\r\n context: C | undefined,\r\n ): Promise<void> {\r\n if (!this.beforeGet) return;\r\n if (!(await this.beforeGet(imageId, context)))\r\n throw new AccessDeniedError(\"get\", imageId);\r\n }\r\n\r\n private async checkBeforePut(context: C | undefined): Promise<void> {\r\n if (!this.beforePut) return;\r\n if (!(await this.beforePut(context))) throw new AccessDeniedError(\"put\");\r\n }\r\n\r\n // ---- write path ------------------------------------------------------\r\n\r\n private async prepareRow(\r\n image: ImageInput,\r\n o: PutBase<C> & { filename?: string | null },\r\n ): Promise<PreparedRow> {\r\n const { data, filename } = await readInput(\r\n image,\r\n o.filename ?? undefined,\r\n this.maxBytes,\r\n );\r\n\r\n let finalData: Buffer;\r\n let mimeType: string;\r\n let width: number | null;\r\n let height: number | null;\r\n let sizeBytes: number;\r\n\r\n if (o.validator) {\r\n if (o.optimize) {\r\n throw new ImageValidationError(\r\n \"optimize is not supported together with a custom validator: the optimize pipeline \" +\r\n \"(resize/re-encode) is image-specific. Validate/transform custom content types yourself before put().\",\r\n );\r\n }\r\n const v = await o.validator.validate(data, { maxBytes: this.maxBytes });\r\n finalData = data;\r\n mimeType = v.mimeType;\r\n width = v.width ?? null;\r\n height = v.height ?? null;\r\n sizeBytes = v.sizeBytes;\r\n } else {\r\n const validated = await validateImage(data, {\r\n maxBytes: this.maxBytes,\r\n maxPixels: this.maxPixels,\r\n allowedFormats: this.allowedFormats,\r\n });\r\n if (o.optimize) {\r\n const opts = o.optimize === true ? {} : o.optimize;\r\n const r = await optimizeImage(data, {\r\n ...opts,\r\n maxBytes: this.maxBytes,\r\n maxPixels: this.maxPixels,\r\n });\r\n finalData = r.data;\r\n mimeType = r.mimeType;\r\n width = r.width;\r\n height = r.height;\r\n sizeBytes = r.sizeBytes;\r\n } else {\r\n finalData = data;\r\n mimeType = validated.mimeType;\r\n width = validated.width;\r\n height = validated.height;\r\n sizeBytes = validated.sizeBytes;\r\n }\r\n }\r\n\r\n return {\r\n data: finalData,\r\n mimeType,\r\n originalFilename: filename ?? null,\r\n sizeBytes,\r\n width,\r\n height,\r\n checksumSha256: createHash(\"sha256\").update(finalData).digest(\"hex\"),\r\n };\r\n }\r\n\r\n /** Validate, optionally optimize, checksum and store one image. Returns its id (a UUID). */\r\n async put(image: ImageInput, options: PutOptions<C> = {}): Promise<string> {\r\n await this.checkBeforePut(options.context);\r\n const row = await this.prepareRow(image, options);\r\n return this.backend.put(row, { connection: options.connection });\r\n }\r\n\r\n /**\r\n * Store many images. Best-effort, not all-or-nothing: one bad image does not\r\n * abort the rest. Check each result's `success` / `error`. Settings apply to\r\n * every item. Valid items are inserted in ONE transaction.\r\n */\r\n async putMany(\r\n images: ImageInput[],\r\n options: PutManyOptions<C> = {},\r\n ): Promise<BatchPutResult[]> {\r\n const { filenames } = options;\r\n if (filenames && filenames.length !== images.length) {\r\n throw new RangeError(\r\n \"filenames must be the same length as images if provided\",\r\n );\r\n }\r\n\r\n const fail = (index: number, error: string): BatchPutResult => ({\r\n index,\r\n imageId: null,\r\n error,\r\n success: false,\r\n });\r\n\r\n if (this.beforePut) {\r\n try {\r\n if (!(await this.beforePut(options.context)))\r\n return images.map((_, i) => fail(i, \"access denied\"));\r\n } catch (exc) {\r\n return images.map((_, i) => fail(i, errText(exc))); // fail closed, per item\r\n }\r\n }\r\n\r\n const results: (BatchPutResult | undefined)[] = new Array(images.length);\r\n const prepared: { index: number; row: PreparedRow }[] = [];\r\n\r\n await mapLimit(images, PREPARE_CONCURRENCY, async (image, i) => {\r\n try {\r\n const row = await this.prepareRow(image, {\r\n ...options,\r\n filename: filenames?.[i] ?? undefined,\r\n });\r\n prepared.push({ index: i, row });\r\n } catch (exc) {\r\n results[i] = fail(i, errText(exc));\r\n }\r\n });\r\n prepared.sort((a, b) => a.index - b.index);\r\n\r\n if (prepared.length > 0) {\r\n try {\r\n const ids = await this.backend.putMany(\r\n prepared.map((p) => p.row),\r\n { connection: options.connection },\r\n );\r\n prepared.forEach((p, n) => {\r\n results[p.index] = {\r\n index: p.index,\r\n imageId: ids[n]!,\r\n error: null,\r\n success: true,\r\n };\r\n });\r\n } catch (exc) {\r\n // The whole DB batch failed; every validated item shared that transaction.\r\n for (const p of prepared)\r\n results[p.index] = fail(p.index, errText(exc));\r\n }\r\n }\r\n return results as BatchPutResult[];\r\n }\r\n\r\n // ---- read path -------------------------------------------------------\r\n\r\n /** Retrieve a full image including bytes. Throws ImageNotFoundError if missing. */\r\n async get(imageId: string, options: GetOptions<C> = {}): Promise<Image> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const record = await this.backend.get(imageId, {\r\n connection: options.connection,\r\n });\r\n if (!record) throw new ImageNotFoundError(imageId);\r\n return {\r\n data: record.data,\r\n mimeType: record.mimeType,\r\n filename: record.originalFilename,\r\n sizeBytes: record.sizeBytes,\r\n width: record.width,\r\n height: record.height,\r\n checksumSha256: record.checksumSha256,\r\n };\r\n }\r\n\r\n /**\r\n * Retrieve many images in one query. Results come back in input order; a\r\n * missing id is `error: \"not found\"`, not an exception. A hook denial or hook\r\n * exception affects only that id.\r\n */\r\n async getMany(\r\n imageIds: string[],\r\n options: GetOptions<C> = {},\r\n ): Promise<BatchGetResult[]> {\r\n const results: (BatchGetResult | undefined)[] = new Array(imageIds.length);\r\n const allowedIdx: number[] = [];\r\n\r\n for (let i = 0; i < imageIds.length; i++) {\r\n const imageId = imageIds[i]!;\r\n if (this.beforeGet) {\r\n try {\r\n if (!(await this.beforeGet(imageId, options.context))) {\r\n results[i] = {\r\n imageId,\r\n image: null,\r\n error: \"access denied\",\r\n success: false,\r\n };\r\n continue;\r\n }\r\n } catch (exc) {\r\n results[i] = {\r\n imageId,\r\n image: null,\r\n error: errText(exc),\r\n success: false,\r\n };\r\n continue;\r\n }\r\n }\r\n allowedIdx.push(i);\r\n }\r\n\r\n if (allowedIdx.length > 0) {\r\n const records = await this.backend.getMany(\r\n allowedIdx.map((i) => imageIds[i]!),\r\n { connection: options.connection },\r\n );\r\n const byId = new Map(records.map((r) => [r.id.toLowerCase(), r]));\r\n for (const i of allowedIdx) {\r\n const imageId = imageIds[i]!;\r\n const r = byId.get(imageId.toLowerCase());\r\n results[i] = r\r\n ? {\r\n imageId,\r\n image: {\r\n data: r.data,\r\n mimeType: r.mimeType,\r\n filename: r.originalFilename,\r\n sizeBytes: r.sizeBytes,\r\n width: r.width,\r\n height: r.height,\r\n checksumSha256: r.checksumSha256,\r\n },\r\n error: null,\r\n success: true,\r\n }\r\n : { imageId, image: null, error: \"not found\", success: false };\r\n }\r\n }\r\n return results as BatchGetResult[];\r\n }\r\n\r\n /**\r\n * Stream an image's bytes in chunks without holding the whole image in memory.\r\n * Not-found is checked eagerly: the returned promise rejects before any chunk\r\n * is read. A concurrent delete mid-stream makes iteration throw StorageError\r\n * rather than silently truncating. Pass `range` for partial reads.\r\n *\r\n * Honest limitation: reduces Node-side memory, not Postgres-side (the server\r\n * still handles the full BYTEA value per chunk request).\r\n */\r\n async getStream(\r\n imageId: string,\r\n options: StreamOptions<C> = {},\r\n ): Promise<AsyncIterable<Buffer>> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const { range } = options;\r\n if (range) {\r\n if (\r\n !Number.isInteger(range.start) ||\r\n range.start < 0 ||\r\n (range.end !== undefined &&\r\n (!Number.isInteger(range.end) || range.end < range.start))\r\n ) {\r\n throw new RangeError(\r\n \"range must be { start >= 0, end >= start } with integer values\",\r\n );\r\n }\r\n }\r\n const stream = await this.backend.getStream(imageId, {\r\n chunkSize: options.chunkSize ?? DEFAULT_STREAM_CHUNK_SIZE,\r\n connection: options.connection,\r\n range: range\r\n ? { start: range.start, end: range.end ?? Number.MAX_SAFE_INTEGER }\r\n : undefined,\r\n });\r\n if (!stream) throw new ImageNotFoundError(imageId);\r\n return stream;\r\n }\r\n\r\n /** Same as getStream(), as a Web `ReadableStream` you can hand straight to `new Response(...)`. */\r\n async toWebStream(\r\n imageId: string,\r\n options: StreamOptions<C> = {},\r\n ): Promise<ReadableStream<Uint8Array>> {\r\n const iterable = await this.getStream(imageId, options);\r\n const iterator = iterable[Symbol.asyncIterator]();\r\n return new ReadableStream<Uint8Array>({\r\n async pull(controller) {\r\n try {\r\n const { value, done } = await iterator.next();\r\n if (done) controller.close();\r\n else\r\n controller.enqueue(\r\n new Uint8Array(value.buffer, value.byteOffset, value.byteLength),\r\n );\r\n } catch (exc) {\r\n controller.error(exc);\r\n }\r\n },\r\n async cancel() {\r\n await iterator.return?.();\r\n },\r\n });\r\n }\r\n\r\n /** Write an image to `destination` chunk by chunk (honouring backpressure). Returns bytes written. Does not end the destination. */\r\n async streamTo(\r\n imageId: string,\r\n destination: WritableLike,\r\n options: StreamOptions<C> = {},\r\n ): Promise<number> {\r\n let total = 0;\r\n for await (const chunk of await this.getStream(imageId, options)) {\r\n const result = destination.write(chunk);\r\n if (result === false && typeof destination.once === \"function\") {\r\n await new Promise<void>((resolve) =>\r\n destination.once!(\"drain\", resolve),\r\n );\r\n } else if (\r\n result &&\r\n typeof (result as Promise<void>).then === \"function\"\r\n ) {\r\n await result;\r\n }\r\n total += chunk.length;\r\n }\r\n return total;\r\n }\r\n\r\n /** Metadata without pulling the bytes. Gated by the same beforeGet hook as get(). */\r\n async metadata(\r\n imageId: string,\r\n options: GetOptions<C> = {},\r\n ): Promise<ImageMetadata> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const r = await this.backend.getMetadata(imageId, {\r\n connection: options.connection,\r\n });\r\n if (!r) throw new ImageNotFoundError(imageId);\r\n return {\r\n imageId: r.id,\r\n mimeType: r.mimeType,\r\n filename: r.originalFilename,\r\n sizeBytes: r.sizeBytes,\r\n width: r.width,\r\n height: r.height,\r\n checksumSha256: r.checksumSha256,\r\n };\r\n }\r\n\r\n /** Whether an image exists. Deliberately NOT gated by beforeGet (returns no data or metadata). */\r\n exists(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n return this.backend.exists(imageId, options);\r\n }\r\n\r\n /** Delete an image. Returns true if it existed. */\r\n delete(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n return this.backend.delete(imageId, options);\r\n }\r\n\r\n /** Delete many in one query. Results are in input order; a missing id is `deleted: false`, not an error. */\r\n async deleteMany(\r\n imageIds: string[],\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<BatchDeleteResult[]> {\r\n if (imageIds.length === 0) return [];\r\n const deleted = new Set(\r\n (await this.backend.deleteMany(imageIds, options)).map((id) =>\r\n id.toLowerCase(),\r\n ),\r\n );\r\n return imageIds.map((imageId) => ({\r\n imageId,\r\n deleted: deleted.has(imageId.toLowerCase()),\r\n error: null,\r\n success: true,\r\n }));\r\n }\r\n\r\n /** Release the pool (if ZeroBucket created it). */\r\n close(): Promise<void> {\r\n return this.backend.close();\r\n }\r\n}\r\n\r\n// ---- input handling ----------------------------------------------------\r\n\r\n/**\r\n * Read any supported input into a Buffer. Streams are consumed in bounded\r\n * chunks and rejected as soon as they pass maxBytes (reported size is then a\r\n * lower bound), rather than buffering an arbitrarily large upload first.\r\n * Files and Blobs are size-checked BEFORE their bytes are read.\r\n */\r\nexport async function readInput(\r\n image: ImageInput,\r\n filename: string | undefined,\r\n maxBytes: number,\r\n): Promise<{ data: Buffer; filename: string | undefined }> {\r\n if (typeof image === \"string\") {\r\n const size = (await stat(image)).size;\r\n if (size > maxBytes) throw new ImageTooLargeError(size, maxBytes);\r\n return {\r\n data: await readFile(image),\r\n filename: filename ?? basename(image),\r\n };\r\n }\r\n if (Buffer.isBuffer(image)) return { data: image, filename };\r\n if (image instanceof Uint8Array)\r\n return {\r\n data: Buffer.from(image.buffer, image.byteOffset, image.byteLength),\r\n filename,\r\n };\r\n if (image instanceof ArrayBuffer)\r\n return { data: Buffer.from(image), filename };\r\n if (typeof Blob !== \"undefined\" && image instanceof Blob) {\r\n if (image.size > maxBytes)\r\n throw new ImageTooLargeError(image.size, maxBytes);\r\n const name = (image as { name?: unknown }).name;\r\n return {\r\n data: Buffer.from(await image.arrayBuffer()),\r\n filename: filename ?? (typeof name === \"string\" ? name : undefined),\r\n };\r\n }\r\n if (\r\n image &&\r\n typeof (image as AsyncIterable<unknown>)[Symbol.asyncIterator] ===\r\n \"function\"\r\n ) {\r\n const chunks: Buffer[] = [];\r\n let total = 0;\r\n for await (const chunk of image as AsyncIterable<Uint8Array | string>) {\r\n const buf =\r\n typeof chunk === \"string\"\r\n ? Buffer.from(chunk)\r\n : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);\r\n total += buf.length;\r\n if (total > maxBytes) {\r\n (image as { destroy?: () => void }).destroy?.();\r\n throw new ImageTooLargeError(total, maxBytes);\r\n }\r\n chunks.push(buf);\r\n }\r\n const p = (image as { path?: unknown }).path;\r\n return {\r\n data: Buffer.concat(chunks, total),\r\n filename: filename ?? (typeof p === \"string\" ? basename(p) : undefined),\r\n };\r\n }\r\n throw new TypeError(\r\n \"Unsupported image input. Pass a Buffer, Uint8Array, file path string, Blob/File, or a readable stream.\",\r\n );\r\n}\r\n\r\nasync function mapLimit<T>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T, index: number) => Promise<void>,\r\n): Promise<void> {\r\n let next = 0;\r\n const workers = Array.from(\r\n { length: Math.min(limit, items.length) },\r\n async () => {\r\n while (next < items.length) {\r\n const i = next++;\r\n await fn(items[i]!, i);\r\n }\r\n },\r\n );\r\n await Promise.all(workers);\r\n}\r\n\r\nfunction errText(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n","/**\r\n * PostgreSQL storage adapter (classic mode).\r\n *\r\n * Uses the EXACT schema the Python package creates (zerobucket_images, including\r\n * the object-storage tiering columns and CHECK constraint), so a Node app and a\r\n * Python app can share one database and read each other's rows.\r\n *\r\n * Lessons from the Python build, applied here from day one:\r\n * - Retry re-runs the WHOLE unit of work on a fresh connection after a full\r\n * ROLLBACK. It never replays a single statement of a multi-statement\r\n * transaction (the \"half-applied put\" hazard).\r\n * - Retry never applies when the caller passes `connection` (the caller owns\r\n * the transaction; restarting it is their decision).\r\n * - Pool acquire has an explicit timeout.\r\n * - Migration runs under an advisory lock so N concurrent first-callers do not\r\n * race on CREATE TABLE.\r\n */\r\nimport type { Pool, PoolClient, PoolConfig } from \"pg\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { OperationEvent, PreparedRow, Queryable } from \"../types.js\";\r\nimport type {\r\n CallOptions,\r\n StorageBackend,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\n\r\n/** 1 MiB. Default chunk size for getStream(). */\r\nexport const DEFAULT_STREAM_CHUNK_SIZE = 1024 * 1024;\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\r\n data BYTEA NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n checksum_sha256 CHAR(64) NOT NULL,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\r\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_checksum ON zerobucket_images (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_created_at ON zerobucket_images (created_at);\r\n\r\n-- Object-storage tiering columns (additive; identical to the Python package).\r\nALTER TABLE zerobucket_images ALTER COLUMN data DROP NOT NULL;\r\nALTER TABLE zerobucket_images\r\n ADD COLUMN IF NOT EXISTS storage_backend TEXT NOT NULL DEFAULT 'postgres';\r\nALTER TABLE zerobucket_images ADD COLUMN IF NOT EXISTS object_storage_bucket TEXT;\r\nALTER TABLE zerobucket_images ADD COLUMN IF NOT EXISTS object_storage_key TEXT;\r\nDO $$\r\nBEGIN\r\n IF NOT EXISTS (\r\n SELECT 1 FROM pg_constraint WHERE conname = 'zerobucket_storage_location_check'\r\n ) THEN\r\n ALTER TABLE zerobucket_images ADD CONSTRAINT zerobucket_storage_location_check\r\n CHECK (\r\n (storage_backend = 'postgres'\r\n AND data IS NOT NULL\r\n AND object_storage_key IS NULL\r\n AND object_storage_bucket IS NULL)\r\n OR\r\n (storage_backend = 'object_storage'\r\n AND data IS NULL\r\n AND object_storage_key IS NOT NULL\r\n AND object_storage_bucket IS NOT NULL)\r\n );\r\n END IF;\r\nEND $$;\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_storage_backend\r\n ON zerobucket_images (storage_backend) WHERE storage_backend != 'postgres';\r\n`;\r\n\r\nconst INSERT = `\r\nINSERT INTO zerobucket_images\r\n (data, mime_type, original_filename, size_bytes, width, height, checksum_sha256)\r\nVALUES ($1, $2, $3, $4, $5, $6, $7)\r\nRETURNING id;`;\r\n\r\nconst SELECT_FULL = `\r\nSELECT id, data, mime_type, original_filename, size_bytes, width, height,\r\n checksum_sha256, storage_backend, object_storage_bucket, object_storage_key\r\nFROM zerobucket_images WHERE id = $1;`;\r\n\r\nconst SELECT_MANY = `\r\nSELECT id, data, mime_type, original_filename, size_bytes, width, height,\r\n checksum_sha256, storage_backend, object_storage_bucket, object_storage_key\r\nFROM zerobucket_images WHERE id = ANY($1::uuid[]);`;\r\n\r\nconst SELECT_METADATA = `\r\nSELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256\r\nFROM zerobucket_images WHERE id = $1;`;\r\n\r\n// substring() is 1-indexed and clamps `length` at the value's end.\r\nconst SELECT_CHUNK = `SELECT substring(data FROM $1::int FOR $2::int) AS chunk FROM zerobucket_images WHERE id = $3;`;\r\nconst SELECT_STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = $1;`;\r\nconst DELETE_RETURNING = `DELETE FROM zerobucket_images WHERE id = $1 RETURNING storage_backend, object_storage_key;`;\r\nconst DELETE_MANY = `DELETE FROM zerobucket_images WHERE id = ANY($1::uuid[]) RETURNING id, storage_backend, object_storage_key;`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = $1;`;\r\n\r\nconst UUID_RE =\r\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\r\n\r\n/** True if `id` is a syntactically valid UUID. Malformed ids can never exist, so they short-circuit to \"not found\". */\r\nexport function isValidId(id: string): boolean {\r\n return typeof id === \"string\" && UUID_RE.test(id);\r\n}\r\n\r\n// SQLSTATEs worth retrying: transient conditions only, never constraint/syntax errors.\r\nconst RETRYABLE_SQLSTATES = new Set([\r\n \"40001\", // serialization_failure\r\n \"40P01\", // deadlock_detected\r\n \"08000\", // connection_exception\r\n \"08003\", // connection_does_not_exist\r\n \"08006\", // connection_failure\r\n \"08001\", // sqlclient_unable_to_establish_sqlconnection\r\n \"08004\", // sqlserver_rejected_establishment_of_sqlconnection\r\n \"57P01\", // admin_shutdown\r\n \"57P02\", // crash_shutdown\r\n \"57P03\", // cannot_connect_now\r\n \"53000\", // insufficient_resources\r\n \"53300\", // too_many_connections\r\n]);\r\nconst RETRYABLE_NODE_CODES = new Set([\r\n \"ECONNRESET\",\r\n \"ECONNREFUSED\",\r\n \"ETIMEDOUT\",\r\n \"EPIPE\",\r\n \"ECONNABORTED\",\r\n]);\r\nconst CONNECTION_MESSAGE_RE =\r\n /connection terminated|connection timeout|timeout exceeded when trying to connect|client has encountered a connection error/i;\r\n\r\n/** Transient (worth retrying) vs. permanent (fails identically every time). */\r\nexport function isRetryable(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const code = (exc as { code?: unknown }).code;\r\n if (typeof code === \"string\") {\r\n if (RETRYABLE_SQLSTATES.has(code) || RETRYABLE_NODE_CODES.has(code))\r\n return true;\r\n }\r\n return CONNECTION_MESSAGE_RE.test(exc.message);\r\n}\r\n\r\n/** Errors after which the pooled connection must be destroyed, not returned to the pool. */\r\nfunction isConnectionLevel(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const code = (exc as { code?: unknown }).code;\r\n if (\r\n typeof code === \"string\" &&\r\n (RETRYABLE_NODE_CODES.has(code) ||\r\n code.startsWith(\"08\") ||\r\n code.startsWith(\"57\"))\r\n ) {\r\n return true;\r\n }\r\n return CONNECTION_MESSAGE_RE.test(exc.message);\r\n}\r\n\r\nconst MAX_BACKOFF_MS = 2000;\r\n\r\nexport function backoffDelayMs(attempt: number, baseDelayMs: number): number {\r\n const exponential = baseDelayMs * 2 ** (attempt - 1);\r\n const jitter = Math.random() * baseDelayMs;\r\n return Math.min(exponential + jitter, MAX_BACKOFF_MS);\r\n}\r\n\r\nexport interface PostgresBackendOptions {\r\n /** postgresql:// connection string. Required unless `pool` is given. */\r\n connectionString?: string;\r\n /** Bring your own pg.Pool (not closed by `close()`). Pool sizing options are then ignored. */\r\n pool?: Pool;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Retries for transient errors on pooled calls. Default 3; 0 disables. */\r\n maxRetries?: number;\r\n /** Base for exponential backoff with jitter (capped at 2s). Default 100. */\r\n retryBaseDelayMs?: number;\r\n poolMinSize?: number;\r\n poolMaxSize?: number;\r\n /** Max wait for a pooled connection. Default 10000. */\r\n poolTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n}\r\n\r\ntype Work<T> = (q: Queryable) => Promise<T>;\r\n\r\nexport class PostgresBackend implements StorageBackend {\r\n private pool: Pool | undefined;\r\n private readonly ownsPool: boolean;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly maxRetries: number;\r\n private readonly retryBaseDelayMs: number;\r\n private readonly autoMigrate: boolean;\r\n\r\n constructor(private readonly opts: PostgresBackendOptions) {\r\n if (!opts.connectionString && !opts.pool) {\r\n throw new TypeError(\r\n \"PostgresBackend requires either `connectionString` or `pool`\",\r\n );\r\n }\r\n this.ownsPool = !opts.pool;\r\n this.pool = opts.pool;\r\n this.maxRetries = opts.maxRetries ?? 3;\r\n this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 100;\r\n this.autoMigrate = opts.autoMigrate ?? true;\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined; // allow a later attempt (e.g. the DB was briefly down)\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n if (!this.pool) {\r\n let pg: { Pool: typeof Pool };\r\n try {\r\n const mod = (await import(\"pg\")) as unknown as {\r\n default?: { Pool: typeof Pool };\r\n Pool: typeof Pool;\r\n };\r\n pg = mod.default ?? mod; // CJS package: ESM gives { default }, some interop gives it directly\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"PostgresBackend requires the 'pg' package. Install it with: npm install pg\",\r\n { cause },\r\n );\r\n }\r\n const config: PoolConfig = {\r\n connectionString: this.opts.connectionString,\r\n min: this.opts.poolMinSize ?? 1,\r\n max: this.opts.poolMaxSize ?? 5,\r\n connectionTimeoutMillis: this.opts.poolTimeoutMs ?? 10_000,\r\n };\r\n this.pool = new pg.Pool(config);\r\n // An idle client erroring (server restart, network drop) must not crash the process.\r\n this.pool.on(\"error\", () => {});\r\n }\r\n try {\r\n if (this.autoMigrate) await this.migrate();\r\n else await this.pool.query(\"SELECT 1\");\r\n } catch (exc) {\r\n if (this.ownsPool) {\r\n await this.pool.end().catch(() => {});\r\n this.pool = undefined;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Could not connect to PostgreSQL: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n }\r\n\r\n /** Create the schema if missing. Serialized across processes with an advisory lock. */\r\n async migrate(): Promise<void> {\r\n const pool = this.pool!;\r\n let client: PoolClient;\r\n try {\r\n client = await pool.connect();\r\n } catch (exc) {\r\n throw new StorageError(`Could not connect to PostgreSQL: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n try {\r\n await client.query(\"BEGIN\");\r\n await client.query(\r\n \"SELECT pg_advisory_xact_lock(hashtext('zerobucket_migrate'))\",\r\n );\r\n await client.query(SCHEMA);\r\n await client.query(\"COMMIT\");\r\n client.release();\r\n } catch (exc) {\r\n await client.query(\"ROLLBACK\").catch(() => {});\r\n client.release(true);\r\n throw new StorageError(`Migration failed: ${msg(exc)}`, { cause: exc });\r\n }\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const pool = this.pool;\r\n this.pool = undefined;\r\n this.readyPromise = undefined;\r\n if (pool && this.ownsPool) await pool.end();\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /**\r\n * Run `work` and return its result, emitting exactly one OperationEvent.\r\n *\r\n * connection given -> run once on it, no BEGIN/COMMIT, no retry.\r\n * connection absent -> pooled; `tx` wraps the work in BEGIN/COMMIT; transient\r\n * failures retry the WHOLE closure on a fresh connection.\r\n */\r\n private async run<T>(\r\n operation: string,\r\n connection: Queryable | undefined,\r\n work: Work<T>,\r\n { tx = false }: { tx?: boolean } = {},\r\n ): Promise<T> {\r\n if (connection) {\r\n const start = performance.now();\r\n try {\r\n const result = await work(connection);\r\n this.emit(operation, start, true, null, 0);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), 0);\r\n throw exc;\r\n }\r\n }\r\n\r\n await this.ready();\r\n const start = performance.now();\r\n let attempt = 0;\r\n for (;;) {\r\n let client: PoolClient | undefined;\r\n let destroy = false;\r\n try {\r\n client = await this.pool!.connect();\r\n if (tx) await client.query(\"BEGIN\");\r\n const result = await work(client);\r\n if (tx) await client.query(\"COMMIT\");\r\n this.emit(operation, start, true, null, attempt);\r\n return result;\r\n } catch (exc) {\r\n if (client && tx) {\r\n try {\r\n await client.query(\"ROLLBACK\");\r\n } catch {\r\n destroy = true; // cannot trust a connection that cannot roll back\r\n }\r\n }\r\n if (isConnectionLevel(exc)) destroy = true;\r\n attempt += 1;\r\n if (attempt > this.maxRetries || !isRetryable(exc)) {\r\n this.emit(operation, start, false, msg(exc), attempt - 1);\r\n throw exc;\r\n }\r\n } finally {\r\n client?.release(destroy);\r\n }\r\n await sleep(backoffDelayMs(attempt, this.retryBaseDelayMs));\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n return await this.run(\"put\", connection, async (q) => {\r\n const res = await q.query(INSERT, rowParams(row));\r\n return String(res.rows[0].id);\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n // One transaction. A transient failure rolls back EVERYTHING, so a retry\r\n // from scratch cannot create duplicates. Statements are awaited one at a\r\n // time on the single connection (queuing concurrent queries on one pg\r\n // client is deprecated and breaks in pg@9), so result order matches input\r\n // order by construction.\r\n return await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n const ids: string[] = [];\r\n for (const r of rows)\r\n ids.push(String((await q.query(INSERT, rowParams(r))).rows[0].id));\r\n return ids;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get\",\r\n connection,\r\n async (q) => (await q.query(SELECT_FULL, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? this.toRecord(row, id) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.run(\r\n \"get_many\",\r\n connection,\r\n async (q) => (await q.query(SELECT_MANY, [valid])).rows,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return rows.map((r) => this.toRecord(r, String(r.id)));\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(SELECT_METADATA, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(SELECT_STREAM_INFO, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n if (info.storage_backend === \"object_storage\")\r\n throw this.tieredWithoutStorage(id, info.object_storage_key);\r\n\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n const self = this;\r\n\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start; // 0-based\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.run(\r\n \"get_stream\",\r\n connection,\r\n async (q) =>\r\n (await q.query(SELECT_CHUNK, [offset + 1, length, id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n const chunk: Buffer = row.chunk;\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n const row = await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => (await q.query(DELETE_RETURNING, [id])).rows[0],\r\n );\r\n // NOTE: if the row was tiered, the object-storage object is not removed yet\r\n // (tiering ships in a later release of this package). Same documented\r\n // orphan behaviour as the Python backend without object_storage configured.\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n try {\r\n const rows = await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => (await q.query(DELETE_MANY, [valid])).rows,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.run(\r\n \"exists\",\r\n connection,\r\n async (q) => ((await q.query(EXISTS, [id])).rowCount ?? 0) > 0,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n // ---- helpers ---------------------------------------------------------\r\n\r\n private toRecord(row: any, id: string): StoredRecord {\r\n if (row.storage_backend === \"object_storage\")\r\n throw this.tieredWithoutStorage(id, row.object_storage_key);\r\n return {\r\n id: String(row.id),\r\n data: row.data as Buffer,\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n private tieredWithoutStorage(id: string, key: string | null): StorageError {\r\n return new StorageError(\r\n `Image ${JSON.stringify(id)} is stored in object storage (key=${JSON.stringify(key)}) ` +\r\n \"but object-storage tiering is not available in this version of the npm package yet. \" +\r\n \"Read it from the Python package (configured with the same bucket), or wait for npm tiering support.\",\r\n );\r\n }\r\n}\r\n\r\nfunction rowParams(r: PreparedRow): unknown[] {\r\n return [\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n ];\r\n}\r\n\r\nfunction wrap(prefix: string, exc: unknown): StorageError {\r\n return exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`${prefix}: ${msg(exc)}`, { cause: exc });\r\n}\r\n\r\nfunction msg(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => setTimeout(resolve, ms));\r\n}\r\n","/**\r\n * Image validation. Never trusts file extensions or client-supplied MIME types:\r\n * the bytes are decoded with sharp (libvips) and the *detected* format is what\r\n * gets validated and stored.\r\n *\r\n * sharp is an optional peer dependency, imported lazily, so `import \"zerobucket\"`\r\n * works without it (e.g. when you only store PDFs through a custom validator).\r\n */\r\nimport {\r\n CorruptedImageError,\r\n ImageTooLargeError,\r\n ImageValidationError,\r\n StorageError,\r\n UnsupportedFormatError,\r\n} from \"./errors.js\";\r\n\r\n/** sharp format name -> canonical MIME type. Deliberately small allowlist. */\r\nconst FORMAT_TO_MIME: Record<string, string> = {\r\n jpeg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n};\r\n\r\nexport const SUPPORTED_FORMATS: readonly string[] = Object.keys(FORMAT_TO_MIME);\r\n\r\n/** Reject images that would decode to more pixels than this, regardless of compressed size (~89 MP). */\r\nexport const DEFAULT_MAX_PIXELS = 89_000_000;\r\n\r\nexport interface ValidatedImage {\r\n mimeType: string;\r\n width: number;\r\n height: number;\r\n sizeBytes: number;\r\n}\r\n\r\ntype Sharp = typeof import(\"sharp\");\r\nlet sharpPromise: Promise<Sharp> | undefined;\r\n\r\n/** Lazily load sharp, with an actionable error if it is not installed. */\r\nexport function loadSharp(): Promise<Sharp> {\r\n sharpPromise ??= import(\"sharp\").then(\r\n // ESM gives { default: sharp }; some CJS interop gives sharp directly.\r\n (m) =>\r\n (m as unknown as { default?: Sharp }).default ?? (m as unknown as Sharp),\r\n (cause) => {\r\n sharpPromise = undefined;\r\n throw new StorageError(\r\n \"Image validation requires the optional dependency 'sharp'. Install it with: npm install sharp\",\r\n { cause },\r\n );\r\n },\r\n );\r\n return sharpPromise;\r\n}\r\n\r\nexport async function validateImage(\r\n data: Buffer,\r\n opts: {\r\n maxBytes: number;\r\n maxPixels?: number;\r\n allowedFormats?: readonly string[];\r\n },\r\n): Promise<ValidatedImage> {\r\n const {\r\n maxBytes,\r\n maxPixels = DEFAULT_MAX_PIXELS,\r\n allowedFormats = SUPPORTED_FORMATS,\r\n } = opts;\r\n const sizeBytes = data.length;\r\n if (sizeBytes > maxBytes) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n if (sizeBytes === 0) throw new CorruptedImageError(\"Image data is empty\");\r\n\r\n const sharp = await loadSharp();\r\n // failOn \"error\": truncated / corrupt bodies throw instead of silently decoding partially.\r\n const make = () =>\r\n sharp(data, { limitInputPixels: maxPixels, failOn: \"error\" });\r\n\r\n let format: string | undefined;\r\n let width: number | undefined;\r\n let height: number | undefined;\r\n try {\r\n const meta = await make().metadata();\r\n format = meta.format;\r\n width = meta.width;\r\n height = meta.height;\r\n } catch (exc) {\r\n if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n throw new CorruptedImageError(\r\n `Could not decode image: ${errMessage(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n\r\n if (\r\n !format ||\r\n !allowedFormats.includes(format) ||\r\n !(format in FORMAT_TO_MIME)\r\n ) {\r\n throw new UnsupportedFormatError(format ?? null, allowedFormats);\r\n }\r\n if (!width || !height)\r\n throw new CorruptedImageError(\"Image has no readable dimensions\");\r\n\r\n // Header parsing is not enough: force a full pixel decode to catch truncated bodies.\r\n try {\r\n await make().stats();\r\n } catch (exc) {\r\n if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n throw new CorruptedImageError(\r\n `Image data is truncated or corrupted: ${errMessage(exc)}`,\r\n {\r\n cause: exc,\r\n },\r\n );\r\n }\r\n\r\n return { mimeType: FORMAT_TO_MIME[format]!, width, height, sizeBytes };\r\n}\r\n\r\nfunction isPixelLimit(exc: unknown): boolean {\r\n return /pixel limit/i.test(errMessage(exc));\r\n}\r\n\r\nexport function errMessage(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n\r\nexport { ImageValidationError };\r\n","/**\r\n * Optimize pipeline: strip metadata, optionally downscale and re-encode.\r\n * Mirrors the Python package's optimize_image(), implemented on sharp.\r\n *\r\n * Differences from the Python/Pillow version, both deliberate:\r\n * - EXIF orientation is applied (auto-rotate) BEFORE metadata is stripped.\r\n * Stripping EXIF without applying it would leave phone photos sideways.\r\n * - Transparent PNG/WebP converted to JPEG is flattened onto white, not black.\r\n */\r\nimport { ImageValidationError } from \"./errors.js\";\r\nimport {\r\n DEFAULT_MAX_PIXELS,\r\n SUPPORTED_FORMATS,\r\n loadSharp,\r\n validateImage,\r\n} from \"./validation.js\";\r\n\r\nexport const DEFAULT_JPEG_QUALITY = 90;\r\nexport const DEFAULT_WEBP_QUALITY = 88;\r\n\r\nexport interface OptimizeOptions {\r\n /** Downscale (aspect ratio preserved, Lanczos) if wider than this. Never upscales. */\r\n maxWidth?: number;\r\n /** \"jpeg\" | \"png\" | \"webp\". Defaults to the source format. */\r\n format?: string;\r\n /** 1-100, JPEG/WebP only (ignored for PNG). */\r\n quality?: number;\r\n}\r\n\r\nexport interface OptimizationResult {\r\n data: Buffer;\r\n mimeType: string;\r\n width: number;\r\n height: number;\r\n sizeBytes: number;\r\n originalSizeBytes: number;\r\n}\r\n\r\nconst FORMAT_ALIASES: Record<string, string> = { jpg: \"jpeg\" };\r\n\r\nexport async function optimizeImage(\r\n data: Buffer,\r\n opts: OptimizeOptions & { maxBytes: number; maxPixels?: number },\r\n): Promise<OptimizationResult> {\r\n const { maxWidth, quality, maxBytes, maxPixels = DEFAULT_MAX_PIXELS } = opts;\r\n const sharp = await loadSharp();\r\n const originalSizeBytes = data.length;\r\n\r\n const base = sharp(data, { limitInputPixels: maxPixels, failOn: \"error\" });\r\n const meta = await base.metadata();\r\n const requested = (opts.format ?? meta.format ?? \"jpeg\").toLowerCase();\r\n const target = FORMAT_ALIASES[requested] ?? requested;\r\n if (!SUPPORTED_FORMATS.includes(target)) {\r\n throw new ImageValidationError(\r\n `Cannot re-encode to unsupported format ${JSON.stringify(target)}. Supported: ${JSON.stringify(\r\n [...SUPPORTED_FORMATS].sort(),\r\n )}`,\r\n );\r\n }\r\n\r\n // .rotate() with no args = auto-orient from EXIF. sharp drops all metadata on output by default.\r\n let pipeline = sharp(data, {\r\n limitInputPixels: maxPixels,\r\n failOn: \"error\",\r\n }).rotate();\r\n if (maxWidth !== undefined) {\r\n pipeline = pipeline.resize({\r\n width: maxWidth,\r\n fit: \"inside\",\r\n withoutEnlargement: true,\r\n kernel: \"lanczos3\",\r\n });\r\n }\r\n\r\n if (target === \"jpeg\") {\r\n pipeline = pipeline\r\n .flatten({ background: \"#ffffff\" })\r\n .jpeg({\r\n quality: quality ?? DEFAULT_JPEG_QUALITY,\r\n mozjpeg: true,\r\n progressive: true,\r\n });\r\n } else if (target === \"webp\") {\r\n pipeline = pipeline.webp({\r\n quality: quality ?? DEFAULT_WEBP_QUALITY,\r\n effort: 6,\r\n });\r\n } else {\r\n pipeline = pipeline.png({ compressionLevel: 9 }); // no lossy knob; quality ignored by design\r\n }\r\n\r\n const out = await pipeline.toBuffer();\r\n\r\n // Defense in depth: re-validate our own output before it is trusted.\r\n const revalidated = await validateImage(out, { maxBytes, maxPixels });\r\n return {\r\n data: out,\r\n mimeType: revalidated.mimeType,\r\n width: revalidated.width,\r\n height: revalidated.height,\r\n sizeBytes: revalidated.sizeBytes,\r\n originalSizeBytes,\r\n };\r\n}\r\n","import { ContentValidationError } from \"../errors.js\";\r\nimport type { ContentValidator, ValidatedContent } from \"./types.js\";\r\n\r\nexport const DEFAULT_MAX_PDF_BYTES = 20 * 1024 * 1024;\r\n\r\n/**\r\n * Validates that bytes look like a PDF: `%PDF-` magic bytes, non-empty, under a\r\n * size ceiling. Deliberately minimal.\r\n *\r\n * Security note: this does NOT parse the PDF object structure and does not\r\n * detect embedded JavaScript, forms or launch actions. Treat it as a sanity\r\n * gate, not a security boundary, if you serve user-uploaded PDFs to other users.\r\n */\r\nexport class PDFValidator implements ContentValidator {\r\n constructor(private readonly opts: { maxBytes?: number } = {}) {}\r\n\r\n validate(data: Buffer, { maxBytes }: { maxBytes: number }): ValidatedContent {\r\n const effectiveMax = Math.min(\r\n maxBytes,\r\n this.opts.maxBytes ?? DEFAULT_MAX_PDF_BYTES,\r\n );\r\n if (data.length === 0)\r\n throw new ContentValidationError(\"PDF data is empty\");\r\n if (data.length > effectiveMax) {\r\n throw new ContentValidationError(\r\n `PDF is ${data.length} bytes, exceeds the maximum of ${effectiveMax} bytes`,\r\n );\r\n }\r\n // The spec allows junk before the header; real readers scan the first 1024 bytes.\r\n if (!data.subarray(0, 1024).includes(\"%PDF-\")) {\r\n throw new ContentValidationError(\r\n \"This does not look like a valid PDF (missing '%PDF-' header). Content is \" +\r\n \"checked by inspecting the actual bytes, not the filename or any client-supplied content type.\",\r\n );\r\n }\r\n return {\r\n mimeType: \"application/pdf\",\r\n sizeBytes: data.length,\r\n width: null,\r\n height: null,\r\n };\r\n }\r\n}\r\n"],"mappings":"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","names":["start"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/adapters/shared.ts","../src/adapters/postgres.ts","../src/adapters/sqlite.ts","../src/validation.ts","../src/optimize.ts","../src/object-storage.ts","../src/validators/pdf.ts"],"sourcesContent":["/**\r\n * The public ZeroBucket SDK entry point.\r\n *\r\n * const images = new ZeroBucket({ connectionString: process.env.DATABASE_URL });\r\n * const id = await images.put(fileBuffer);\r\n * const image = await images.get(id);\r\n *\r\n * The developer never thinks about BYTEA, checksums or connection pooling.\r\n */\r\nimport { createHash } from \"node:crypto\";\r\nimport { readFile, stat } from \"node:fs/promises\";\r\nimport { basename } from \"node:path\";\r\nimport type { Pool } from \"pg\";\r\nimport type { CallOptions, StorageBackend } from \"./adapters/base.js\";\r\nimport type { ObjectStorageLike } from \"./object-storage.js\";\r\nimport { PostgresBackend } from \"./adapters/postgres.js\";\r\nimport { SQLiteBackend } from \"./adapters/sqlite.js\";\r\nimport { DEFAULT_STREAM_CHUNK_SIZE } from \"./adapters/shared.js\";\r\nimport {\r\n AccessDeniedError,\r\n ImageNotFoundError,\r\n ImageTooLargeError,\r\n ImageValidationError,\r\n StorageError,\r\n} from \"./errors.js\";\r\nimport { type OptimizeOptions, optimizeImage } from \"./optimize.js\";\r\nimport type {\r\n BatchDeleteResult,\r\n BatchGetResult,\r\n BatchPutResult,\r\n Image,\r\n ImageMetadata,\r\n OperationEvent,\r\n PreparedRow,\r\n Connection,\r\n} from \"./types.js\";\r\nimport {\r\n DEFAULT_MAX_PIXELS,\r\n SUPPORTED_FORMATS,\r\n validateImage,\r\n} from \"./validation.js\";\r\nimport type { ContentValidator } from \"./validators/types.js\";\r\n\r\n/** 8 MiB: a practical ceiling for \"small app\" images. Override with `maxBytes`. */\r\nexport const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;\r\n\r\n/**\r\n * What put() accepts:\r\n * - Buffer / Uint8Array / ArrayBuffer: the bytes themselves\r\n * - string: a filesystem PATH (never raw content)\r\n * - Blob / File (e.g. from `request.formData()`)\r\n * - Node Readable or web ReadableStream (read in bounded chunks)\r\n */\r\nexport type ImageInput =\r\n | Buffer\r\n | Uint8Array\r\n | ArrayBuffer\r\n | string\r\n | Blob\r\n | AsyncIterable<Uint8Array | string>;\r\n\r\nexport type BeforeGetHook<C = unknown> = (\r\n imageId: string,\r\n context: C | undefined,\r\n) => boolean | Promise<boolean>;\r\nexport type BeforePutHook<C = unknown> = (\r\n context: C | undefined,\r\n) => boolean | Promise<boolean>;\r\n\r\nexport interface ZeroBucketOptions<C = unknown> {\r\n /** PostgreSQL connection string. */\r\n connectionString?: string;\r\n /** Bring your own pg.Pool. It is NOT closed by close(). */\r\n pool?: Pool;\r\n /**\r\n * Use SQLite instead of PostgreSQL: a database file path (or \":memory:\"). Needs `npm install better-sqlite3`.\r\n * `dedup`, `objectStorage`, `autoMigrate` and `onOperation` apply to it too. Same file format as the Python\r\n * package's SQLiteBackend, so both can share one `.db` file.\r\n */\r\n sqlite?: string;\r\n /** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite`. */\r\n backend?: StorageBackend;\r\n\r\n /** Max accepted size in bytes. Default 8 MiB. */\r\n maxBytes?: number;\r\n /** Decoded-pixel ceiling that rejects decompression bombs. Default ~89 MP. */\r\n maxPixels?: number;\r\n /** Accepted image formats. Default [\"jpeg\", \"png\", \"webp\"]. */\r\n allowedFormats?: readonly string[];\r\n\r\n autoMigrate?: boolean;\r\n maxRetries?: number;\r\n retryBaseDelayMs?: number;\r\n poolMinSize?: number;\r\n poolMaxSize?: number;\r\n poolTimeoutMs?: number;\r\n /** Fire-and-forget observability. Exceptions thrown inside it are swallowed. */\r\n onOperation?: (event: OperationEvent) => void;\r\n\r\n /**\r\n * S3-compatible object storage, enabling `tierToObjectStorage()` and transparent\r\n * reads of images the Python or Node package tiered. Configure it with the same\r\n * bucket and credentials used to tier. Without it, reading a tiered image throws\r\n * a clear StorageError.\r\n */\r\n objectStorage?: ObjectStorageLike;\r\n\r\n /**\r\n * Content-addressed mode: identical bytes are stored once and shared by reference, each image\r\n * keeping its own id and filename. Cannot be combined with `objectStorage`. Uses separate tables\r\n * (zerobucket_blobs / zerobucket_image_refs); see `migrateClassicToDedup()` for existing data.\r\n */\r\n dedup?: boolean;\r\n\r\n /**\r\n * Authorization hook for get / getMany (per id) / getStream / streamTo / metadata.\r\n * Return false to deny. A hook that THROWS fails closed: the exception\r\n * propagates (or is captured per item in getMany) and is never treated as allow.\r\n * NOT called for exists().\r\n */\r\n beforeGet?: BeforeGetHook<C>;\r\n /** Authorization hook for put / putMany (evaluated once per call). Same fail-closed rule. */\r\n beforePut?: BeforePutHook<C>;\r\n}\r\n\r\ninterface PutBase<C> {\r\n /** Display filename stored in metadata (never used for validation). */\r\n filename?: string;\r\n /** `true` = strip metadata only; or pass { maxWidth, format, quality } to resize/re-encode. */\r\n optimize?: boolean | OptimizeOptions;\r\n /** Store non-image content through the same machinery. Incompatible with `optimize`. */\r\n validator?: ContentValidator;\r\n /** Join your own transaction. ZeroBucket then does not commit, roll back or retry. */\r\n connection?: Connection;\r\n /** Passed to beforePut. */\r\n context?: C;\r\n}\r\nexport type PutOptions<C = unknown> = PutBase<C>;\r\nexport interface PutManyOptions<C = unknown> extends Omit<\r\n PutBase<C>,\r\n \"filename\"\r\n> {\r\n /** Same length as `images`; use null/undefined entries to fall back to each input's own name. */\r\n filenames?: (string | null | undefined)[];\r\n}\r\nexport interface GetOptions<C = unknown> {\r\n connection?: Connection;\r\n context?: C;\r\n}\r\nexport interface StreamOptions<C = unknown> extends GetOptions<C> {\r\n /** Bytes per chunk. Default 1 MiB. */\r\n chunkSize?: number;\r\n /** Inclusive byte range, e.g. { start: 0, end: 1023 }. `end` is clamped to the object size. */\r\n range?: { start: number; end?: number };\r\n}\r\n\r\n/** Anything with write(); e.g. fs.WriteStream, http.ServerResponse, or a custom sink. Backpressure is honoured. */\r\nexport interface WritableLike {\r\n write(chunk: Buffer): boolean | void | Promise<void>;\r\n once?(event: \"drain\", listener: () => void): unknown;\r\n}\r\n\r\n/** Bounded concurrency for per-item validation work. */\r\nconst PREPARE_CONCURRENCY = 4;\r\n\r\nexport class ZeroBucket<C = unknown> {\r\n private readonly backend: StorageBackend;\r\n private readonly maxBytes: number;\r\n private readonly maxPixels: number;\r\n private readonly allowedFormats: readonly string[];\r\n private readonly beforeGet: BeforeGetHook<C> | undefined;\r\n private readonly beforePut: BeforePutHook<C> | undefined;\r\n\r\n constructor(options: ZeroBucketOptions<C> = {}) {\r\n if (\r\n options.sqlite !== undefined &&\r\n (options.connectionString || options.pool)\r\n ) {\r\n throw new TypeError(\r\n \"Choose one database: `sqlite` OR `connectionString`/`pool`, not both.\",\r\n );\r\n }\r\n this.backend =\r\n options.backend ??\r\n (options.sqlite !== undefined\r\n ? new SQLiteBackend({\r\n path: options.sqlite,\r\n autoMigrate: options.autoMigrate,\r\n dedup: options.dedup,\r\n objectStorage: options.objectStorage,\r\n onOperation: options.onOperation,\r\n })\r\n : new PostgresBackend({\r\n connectionString: options.connectionString,\r\n pool: options.pool,\r\n autoMigrate: options.autoMigrate,\r\n maxRetries: options.maxRetries,\r\n retryBaseDelayMs: options.retryBaseDelayMs,\r\n poolMinSize: options.poolMinSize,\r\n poolMaxSize: options.poolMaxSize,\r\n poolTimeoutMs: options.poolTimeoutMs,\r\n onOperation: options.onOperation,\r\n objectStorage: options.objectStorage,\r\n dedup: options.dedup,\r\n }));\r\n this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\r\n this.maxPixels = options.maxPixels ?? DEFAULT_MAX_PIXELS;\r\n this.allowedFormats = options.allowedFormats ?? SUPPORTED_FORMATS;\r\n this.beforeGet = options.beforeGet;\r\n this.beforePut = options.beforePut;\r\n }\r\n\r\n /** Construct AND connect/migrate immediately, so a bad DATABASE_URL fails at startup, not on first request. */\r\n static async connect<C = unknown>(\r\n options: ZeroBucketOptions<C> = {},\r\n ): Promise<ZeroBucket<C>> {\r\n const zb = new ZeroBucket<C>(options);\r\n await zb.ready();\r\n return zb;\r\n }\r\n\r\n /** Resolves once connected and migrated. Called automatically on first use. */\r\n ready(): Promise<void> {\r\n return this.backend.ready();\r\n }\r\n\r\n // ---- hooks -----------------------------------------------------------\r\n\r\n private async checkBeforeGet(\r\n imageId: string,\r\n context: C | undefined,\r\n ): Promise<void> {\r\n if (!this.beforeGet) return;\r\n if (!(await this.beforeGet(imageId, context)))\r\n throw new AccessDeniedError(\"get\", imageId);\r\n }\r\n\r\n private async checkBeforePut(context: C | undefined): Promise<void> {\r\n if (!this.beforePut) return;\r\n if (!(await this.beforePut(context))) throw new AccessDeniedError(\"put\");\r\n }\r\n\r\n // ---- write path ------------------------------------------------------\r\n\r\n private async prepareRow(\r\n image: ImageInput,\r\n o: PutBase<C> & { filename?: string | null },\r\n ): Promise<PreparedRow> {\r\n const { data, filename } = await readInput(\r\n image,\r\n o.filename ?? undefined,\r\n this.maxBytes,\r\n );\r\n\r\n let finalData: Buffer;\r\n let mimeType: string;\r\n let width: number | null;\r\n let height: number | null;\r\n let sizeBytes: number;\r\n\r\n if (o.validator) {\r\n if (o.optimize) {\r\n throw new ImageValidationError(\r\n \"optimize is not supported together with a custom validator: the optimize pipeline \" +\r\n \"(resize/re-encode) is image-specific. Validate/transform custom content types yourself before put().\",\r\n );\r\n }\r\n const v = await o.validator.validate(data, { maxBytes: this.maxBytes });\r\n finalData = data;\r\n mimeType = v.mimeType;\r\n width = v.width ?? null;\r\n height = v.height ?? null;\r\n sizeBytes = v.sizeBytes;\r\n } else {\r\n const validated = await validateImage(data, {\r\n maxBytes: this.maxBytes,\r\n maxPixels: this.maxPixels,\r\n allowedFormats: this.allowedFormats,\r\n });\r\n if (o.optimize) {\r\n const opts = o.optimize === true ? {} : o.optimize;\r\n const r = await optimizeImage(data, {\r\n ...opts,\r\n maxBytes: this.maxBytes,\r\n maxPixels: this.maxPixels,\r\n });\r\n finalData = r.data;\r\n mimeType = r.mimeType;\r\n width = r.width;\r\n height = r.height;\r\n sizeBytes = r.sizeBytes;\r\n } else {\r\n finalData = data;\r\n mimeType = validated.mimeType;\r\n width = validated.width;\r\n height = validated.height;\r\n sizeBytes = validated.sizeBytes;\r\n }\r\n }\r\n\r\n return {\r\n data: finalData,\r\n mimeType,\r\n originalFilename: filename ?? null,\r\n sizeBytes,\r\n width,\r\n height,\r\n checksumSha256: createHash(\"sha256\").update(finalData).digest(\"hex\"),\r\n };\r\n }\r\n\r\n /** Validate, optionally optimize, checksum and store one image. Returns its id (a UUID). */\r\n async put(image: ImageInput, options: PutOptions<C> = {}): Promise<string> {\r\n await this.checkBeforePut(options.context);\r\n const row = await this.prepareRow(image, options);\r\n return this.backend.put(row, { connection: options.connection });\r\n }\r\n\r\n /**\r\n * Store many images. Best-effort, not all-or-nothing: one bad image does not\r\n * abort the rest. Check each result's `success` / `error`. Settings apply to\r\n * every item. Valid items are inserted in ONE transaction.\r\n */\r\n async putMany(\r\n images: ImageInput[],\r\n options: PutManyOptions<C> = {},\r\n ): Promise<BatchPutResult[]> {\r\n const { filenames } = options;\r\n if (filenames && filenames.length !== images.length) {\r\n throw new RangeError(\r\n \"filenames must be the same length as images if provided\",\r\n );\r\n }\r\n\r\n const fail = (index: number, error: string): BatchPutResult => ({\r\n index,\r\n imageId: null,\r\n error,\r\n success: false,\r\n });\r\n\r\n if (this.beforePut) {\r\n try {\r\n if (!(await this.beforePut(options.context)))\r\n return images.map((_, i) => fail(i, \"access denied\"));\r\n } catch (exc) {\r\n return images.map((_, i) => fail(i, errText(exc))); // fail closed, per item\r\n }\r\n }\r\n\r\n const results: (BatchPutResult | undefined)[] = new Array(images.length);\r\n const prepared: { index: number; row: PreparedRow }[] = [];\r\n\r\n await mapLimit(images, PREPARE_CONCURRENCY, async (image, i) => {\r\n try {\r\n const row = await this.prepareRow(image, {\r\n ...options,\r\n filename: filenames?.[i] ?? undefined,\r\n });\r\n prepared.push({ index: i, row });\r\n } catch (exc) {\r\n results[i] = fail(i, errText(exc));\r\n }\r\n });\r\n prepared.sort((a, b) => a.index - b.index);\r\n\r\n if (prepared.length > 0) {\r\n try {\r\n const ids = await this.backend.putMany(\r\n prepared.map((p) => p.row),\r\n { connection: options.connection },\r\n );\r\n prepared.forEach((p, n) => {\r\n results[p.index] = {\r\n index: p.index,\r\n imageId: ids[n]!,\r\n error: null,\r\n success: true,\r\n };\r\n });\r\n } catch (exc) {\r\n // The whole DB batch failed; every validated item shared that transaction.\r\n for (const p of prepared)\r\n results[p.index] = fail(p.index, errText(exc));\r\n }\r\n }\r\n return results as BatchPutResult[];\r\n }\r\n\r\n // ---- read path -------------------------------------------------------\r\n\r\n /** Retrieve a full image including bytes. Throws ImageNotFoundError if missing. */\r\n async get(imageId: string, options: GetOptions<C> = {}): Promise<Image> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const record = await this.backend.get(imageId, {\r\n connection: options.connection,\r\n });\r\n if (!record) throw new ImageNotFoundError(imageId);\r\n return {\r\n data: record.data,\r\n mimeType: record.mimeType,\r\n filename: record.originalFilename,\r\n sizeBytes: record.sizeBytes,\r\n width: record.width,\r\n height: record.height,\r\n checksumSha256: record.checksumSha256,\r\n };\r\n }\r\n\r\n /**\r\n * Retrieve many images in one query. Results come back in input order; a\r\n * missing id is `error: \"not found\"`, not an exception. A hook denial or hook\r\n * exception affects only that id.\r\n */\r\n async getMany(\r\n imageIds: string[],\r\n options: GetOptions<C> = {},\r\n ): Promise<BatchGetResult[]> {\r\n const results: (BatchGetResult | undefined)[] = new Array(imageIds.length);\r\n const allowedIdx: number[] = [];\r\n\r\n for (let i = 0; i < imageIds.length; i++) {\r\n const imageId = imageIds[i]!;\r\n if (this.beforeGet) {\r\n try {\r\n if (!(await this.beforeGet(imageId, options.context))) {\r\n results[i] = {\r\n imageId,\r\n image: null,\r\n error: \"access denied\",\r\n success: false,\r\n };\r\n continue;\r\n }\r\n } catch (exc) {\r\n results[i] = {\r\n imageId,\r\n image: null,\r\n error: errText(exc),\r\n success: false,\r\n };\r\n continue;\r\n }\r\n }\r\n allowedIdx.push(i);\r\n }\r\n\r\n if (allowedIdx.length > 0) {\r\n const records = await this.backend.getMany(\r\n allowedIdx.map((i) => imageIds[i]!),\r\n { connection: options.connection },\r\n );\r\n const byId = new Map(records.map((r) => [r.id.toLowerCase(), r]));\r\n for (const i of allowedIdx) {\r\n const imageId = imageIds[i]!;\r\n const r = byId.get(imageId.toLowerCase());\r\n results[i] = r\r\n ? {\r\n imageId,\r\n image: {\r\n data: r.data,\r\n mimeType: r.mimeType,\r\n filename: r.originalFilename,\r\n sizeBytes: r.sizeBytes,\r\n width: r.width,\r\n height: r.height,\r\n checksumSha256: r.checksumSha256,\r\n },\r\n error: null,\r\n success: true,\r\n }\r\n : { imageId, image: null, error: \"not found\", success: false };\r\n }\r\n }\r\n return results as BatchGetResult[];\r\n }\r\n\r\n /**\r\n * Stream an image's bytes in chunks without holding the whole image in memory.\r\n * Not-found is checked eagerly: the returned promise rejects before any chunk\r\n * is read. A concurrent delete mid-stream makes iteration throw StorageError\r\n * rather than silently truncating. Pass `range` for partial reads.\r\n *\r\n * Honest limitation: reduces Node-side memory, not Postgres-side (the server\r\n * still handles the full BYTEA value per chunk request).\r\n */\r\n async getStream(\r\n imageId: string,\r\n options: StreamOptions<C> = {},\r\n ): Promise<AsyncIterable<Buffer>> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const { range } = options;\r\n if (range) {\r\n if (\r\n !Number.isInteger(range.start) ||\r\n range.start < 0 ||\r\n (range.end !== undefined &&\r\n (!Number.isInteger(range.end) || range.end < range.start))\r\n ) {\r\n throw new RangeError(\r\n \"range must be { start >= 0, end >= start } with integer values\",\r\n );\r\n }\r\n }\r\n const stream = await this.backend.getStream(imageId, {\r\n chunkSize: options.chunkSize ?? DEFAULT_STREAM_CHUNK_SIZE,\r\n connection: options.connection,\r\n range: range\r\n ? { start: range.start, end: range.end ?? Number.MAX_SAFE_INTEGER }\r\n : undefined,\r\n });\r\n if (!stream) throw new ImageNotFoundError(imageId);\r\n return stream;\r\n }\r\n\r\n /** Same as getStream(), as a Web `ReadableStream` you can hand straight to `new Response(...)`. */\r\n async toWebStream(\r\n imageId: string,\r\n options: StreamOptions<C> = {},\r\n ): Promise<ReadableStream<Uint8Array>> {\r\n const iterable = await this.getStream(imageId, options);\r\n const iterator = iterable[Symbol.asyncIterator]();\r\n return new ReadableStream<Uint8Array>({\r\n async pull(controller) {\r\n try {\r\n const { value, done } = await iterator.next();\r\n if (done) controller.close();\r\n else\r\n controller.enqueue(\r\n new Uint8Array(value.buffer, value.byteOffset, value.byteLength),\r\n );\r\n } catch (exc) {\r\n controller.error(exc);\r\n }\r\n },\r\n async cancel() {\r\n await iterator.return?.();\r\n },\r\n });\r\n }\r\n\r\n /** Write an image to `destination` chunk by chunk (honouring backpressure). Returns bytes written. Does not end the destination. */\r\n async streamTo(\r\n imageId: string,\r\n destination: WritableLike,\r\n options: StreamOptions<C> = {},\r\n ): Promise<number> {\r\n let total = 0;\r\n for await (const chunk of await this.getStream(imageId, options)) {\r\n const result = destination.write(chunk);\r\n if (result === false && typeof destination.once === \"function\") {\r\n await new Promise<void>((resolve) =>\r\n destination.once!(\"drain\", resolve),\r\n );\r\n } else if (\r\n result &&\r\n typeof (result as Promise<void>).then === \"function\"\r\n ) {\r\n await result;\r\n }\r\n total += chunk.length;\r\n }\r\n return total;\r\n }\r\n\r\n /** Metadata without pulling the bytes. Gated by the same beforeGet hook as get(). */\r\n async metadata(\r\n imageId: string,\r\n options: GetOptions<C> = {},\r\n ): Promise<ImageMetadata> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const r = await this.backend.getMetadata(imageId, {\r\n connection: options.connection,\r\n });\r\n if (!r) throw new ImageNotFoundError(imageId);\r\n return {\r\n imageId: r.id,\r\n mimeType: r.mimeType,\r\n filename: r.originalFilename,\r\n sizeBytes: r.sizeBytes,\r\n width: r.width,\r\n height: r.height,\r\n checksumSha256: r.checksumSha256,\r\n };\r\n }\r\n\r\n /** Whether an image exists. Deliberately NOT gated by beforeGet (returns no data or metadata). */\r\n exists(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n return this.backend.exists(imageId, options);\r\n }\r\n\r\n /** Delete an image. Returns true if it existed. */\r\n delete(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n return this.backend.delete(imageId, options);\r\n }\r\n\r\n /** Delete many in one query. Results are in input order; a missing id is `deleted: false`, not an error. */\r\n async deleteMany(\r\n imageIds: string[],\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<BatchDeleteResult[]> {\r\n if (imageIds.length === 0) return [];\r\n const deleted = new Set(\r\n (await this.backend.deleteMany(imageIds, options)).map((id) =>\r\n id.toLowerCase(),\r\n ),\r\n );\r\n return imageIds.map((imageId) => ({\r\n imageId,\r\n deleted: deleted.has(imageId.toLowerCase()),\r\n error: null,\r\n success: true,\r\n }));\r\n }\r\n\r\n /**\r\n * Move an image's bytes from Postgres into object storage; reads, streams and\r\n * deletes keep working transparently. Returns true if tiered now, false if it was\r\n * already tiered (safe to re-run in a backfill). Throws ImageNotFoundError if missing.\r\n *\r\n * The upload runs inside the database transaction: if it fails, the row is left\r\n * completely untouched. Explicit and opt-in: put() never tiers automatically.\r\n * Not gated by beforeGet/beforePut (it is a maintenance operation, as in the Python package).\r\n */\r\n async tierToObjectStorage(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n const result = await this.backend.tierToObjectStorage(imageId, options);\r\n if (result === null) throw new ImageNotFoundError(imageId);\r\n return result;\r\n }\r\n\r\n /**\r\n * One-time, non-destructive copy of classic-mode images into a dedup instance's tables, keeping every\r\n * id. Call it on an instance created with `dedup: true`. Safe to re-run. See PostgresBackend for details.\r\n */\r\n async migrateClassicToDedup(): ReturnType<\r\n NonNullable<StorageBackend[\"migrateClassicToDedup\"]>\r\n > {\r\n if (!this.backend.migrateClassicToDedup) {\r\n throw new StorageError(\r\n \"This backend does not support migrateClassicToDedup().\",\r\n );\r\n }\r\n return this.backend.migrateClassicToDedup();\r\n }\r\n\r\n /** Release the pool (if ZeroBucket created it). */\r\n close(): Promise<void> {\r\n return this.backend.close();\r\n }\r\n}\r\n\r\n// ---- input handling ----------------------------------------------------\r\n\r\n/**\r\n * Read any supported input into a Buffer. Streams are consumed in bounded\r\n * chunks and rejected as soon as they pass maxBytes (reported size is then a\r\n * lower bound), rather than buffering an arbitrarily large upload first.\r\n * Files and Blobs are size-checked BEFORE their bytes are read.\r\n */\r\nexport async function readInput(\r\n image: ImageInput,\r\n filename: string | undefined,\r\n maxBytes: number,\r\n): Promise<{ data: Buffer; filename: string | undefined }> {\r\n if (typeof image === \"string\") {\r\n const size = (await stat(image)).size;\r\n if (size > maxBytes) throw new ImageTooLargeError(size, maxBytes);\r\n return {\r\n data: await readFile(image),\r\n filename: filename ?? basename(image),\r\n };\r\n }\r\n if (Buffer.isBuffer(image)) return { data: image, filename };\r\n if (image instanceof Uint8Array)\r\n return {\r\n data: Buffer.from(image.buffer, image.byteOffset, image.byteLength),\r\n filename,\r\n };\r\n if (image instanceof ArrayBuffer)\r\n return { data: Buffer.from(image), filename };\r\n if (typeof Blob !== \"undefined\" && image instanceof Blob) {\r\n if (image.size > maxBytes)\r\n throw new ImageTooLargeError(image.size, maxBytes);\r\n const name = (image as { name?: unknown }).name;\r\n return {\r\n data: Buffer.from(await image.arrayBuffer()),\r\n filename: filename ?? (typeof name === \"string\" ? name : undefined),\r\n };\r\n }\r\n if (\r\n image &&\r\n typeof (image as AsyncIterable<unknown>)[Symbol.asyncIterator] ===\r\n \"function\"\r\n ) {\r\n const chunks: Buffer[] = [];\r\n let total = 0;\r\n for await (const chunk of image as AsyncIterable<Uint8Array | string>) {\r\n const buf =\r\n typeof chunk === \"string\"\r\n ? Buffer.from(chunk)\r\n : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);\r\n total += buf.length;\r\n if (total > maxBytes) {\r\n (image as { destroy?: () => void }).destroy?.();\r\n throw new ImageTooLargeError(total, maxBytes);\r\n }\r\n chunks.push(buf);\r\n }\r\n const p = (image as { path?: unknown }).path;\r\n return {\r\n data: Buffer.concat(chunks, total),\r\n filename: filename ?? (typeof p === \"string\" ? basename(p) : undefined),\r\n };\r\n }\r\n throw new TypeError(\r\n \"Unsupported image input. Pass a Buffer, Uint8Array, file path string, Blob/File, or a readable stream.\",\r\n );\r\n}\r\n\r\nasync function mapLimit<T>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T, index: number) => Promise<void>,\r\n): Promise<void> {\r\n let next = 0;\r\n const workers = Array.from(\r\n { length: Math.min(limit, items.length) },\r\n async () => {\r\n while (next < items.length) {\r\n const i = next++;\r\n await fn(items[i]!, i);\r\n }\r\n },\r\n );\r\n await Promise.all(workers);\r\n}\r\n\r\nfunction errText(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n","/** Helpers shared by every storage adapter (kept here so Postgres and SQLite cannot drift apart). */\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type { StoredRecord } from \"./base.js\";\r\n\r\n/** 1 MiB. Default chunk size for getStream(). */\r\nexport const DEFAULT_STREAM_CHUNK_SIZE = 1024 * 1024;\r\n\r\nconst UUID_RE =\r\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\r\n\r\n/** True if `id` is a syntactically valid UUID. Malformed ids can never exist, so they short-circuit to \"not found\". */\r\nexport function isValidId(id: string): boolean {\r\n return typeof id === \"string\" && UUID_RE.test(id);\r\n}\r\n\r\nexport function msg(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n\r\nexport function wrap(prefix: string, exc: unknown): StorageError {\r\n return exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`${prefix}: ${msg(exc)}`, { cause: exc });\r\n}\r\n\r\nexport function sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => setTimeout(resolve, ms));\r\n}\r\n\r\nexport async function mapLimit<T, R>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T) => Promise<R>,\r\n): Promise<R[]> {\r\n const out: R[] = new Array(items.length);\r\n let next = 0;\r\n await Promise.all(\r\n Array.from({ length: Math.min(limit, items.length) }, async () => {\r\n while (next < items.length) {\r\n const i = next++;\r\n out[i] = await fn(items[i]!);\r\n }\r\n }),\r\n );\r\n return out;\r\n}\r\n\r\nexport function tieredWithoutStorage(\r\n id: string,\r\n key: string | null,\r\n): StorageError {\r\n return new StorageError(\r\n `Image ${JSON.stringify(id)} is stored in object storage (key=${JSON.stringify(key)}) ` +\r\n \"but this ZeroBucket was constructed without `objectStorage`. Configure it with the \" +\r\n \"same bucket and credentials that were used to tier this image.\",\r\n );\r\n}\r\n\r\n/** Turn a DB row into a record, fetching the bytes from object storage if the row is tiered. */\r\nexport async function hydrateRow(\r\n row: any,\r\n id: string,\r\n os: ObjectStorageLike | undefined,\r\n): Promise<StoredRecord> {\r\n let data: Buffer;\r\n if (row.storage_backend === \"object_storage\") {\r\n if (!os) throw tieredWithoutStorage(id, row.object_storage_key);\r\n data = await os.download(row.object_storage_key);\r\n if (data.length !== row.size_bytes) {\r\n throw new StorageError(\r\n `Object ${JSON.stringify(row.object_storage_key)} for image ${JSON.stringify(id)} has ${data.length} bytes ` +\r\n `but the database records ${row.size_bytes}. The stored object is corrupted or was replaced.`,\r\n );\r\n }\r\n } else {\r\n data = row.data as Buffer;\r\n }\r\n return {\r\n id: String(row.id),\r\n data,\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n}\r\n\r\n/** Ranged reads straight from the object store; fails loudly rather than ever truncating. */\r\nexport async function* streamFromObjectStorage(\r\n os: ObjectStorageLike,\r\n id: string,\r\n key: string,\r\n start: number,\r\n end: number,\r\n chunkSize: number,\r\n): AsyncGenerator<Buffer> {\r\n let offset = start;\r\n while (offset <= end) {\r\n const last = Math.min(offset + chunkSize - 1, end);\r\n const expected = last - offset + 1;\r\n const chunk = await os.downloadRange(key, offset, last);\r\n if (chunk.length !== expected) {\r\n throw new StorageError(\r\n `Object ${JSON.stringify(key)} for image ${JSON.stringify(id)} returned ${chunk.length} bytes for a ${expected}-byte range; ` +\r\n \"the stored object is shorter than the database records.\",\r\n );\r\n }\r\n yield chunk;\r\n offset = last + 1;\r\n }\r\n}\r\n","/**\r\n * PostgreSQL storage adapter (classic mode).\r\n *\r\n * Uses the EXACT schema the Python package creates (zerobucket_images, including\r\n * the object-storage tiering columns and CHECK constraint), so a Node app and a\r\n * Python app can share one database and read each other's rows.\r\n *\r\n * Lessons from the Python build, applied here from day one:\r\n * - Retry re-runs the WHOLE unit of work on a fresh connection after a full\r\n * ROLLBACK. It never replays a single statement of a multi-statement\r\n * transaction (the \"half-applied put\" hazard).\r\n * - Retry never applies when the caller passes `connection` (the caller owns\r\n * the transaction; restarting it is their decision).\r\n * - Pool acquire has an explicit timeout.\r\n * - Migration runs under an advisory lock so N concurrent first-callers do not\r\n * race on CREATE TABLE.\r\n */\r\nimport type { Pool, PoolClient, PoolConfig } from \"pg\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type {\r\n Connection,\r\n OperationEvent,\r\n PreparedRow,\r\n Queryable,\r\n} from \"../types.js\";\r\nimport {\r\n DEFAULT_STREAM_CHUNK_SIZE,\r\n hydrateRow,\r\n isValidId,\r\n mapLimit,\r\n msg,\r\n sleep,\r\n streamFromObjectStorage,\r\n tieredWithoutStorage,\r\n wrap,\r\n} from \"./shared.js\";\r\nimport type {\r\n CallOptions,\r\n StorageBackend,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\r\n data BYTEA NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n checksum_sha256 CHAR(64) NOT NULL,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\r\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_checksum ON zerobucket_images (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_created_at ON zerobucket_images (created_at);\r\n\r\n-- Object-storage tiering columns (additive; identical to the Python package).\r\nALTER TABLE zerobucket_images ALTER COLUMN data DROP NOT NULL;\r\nALTER TABLE zerobucket_images\r\n ADD COLUMN IF NOT EXISTS storage_backend TEXT NOT NULL DEFAULT 'postgres';\r\nALTER TABLE zerobucket_images ADD COLUMN IF NOT EXISTS object_storage_bucket TEXT;\r\nALTER TABLE zerobucket_images ADD COLUMN IF NOT EXISTS object_storage_key TEXT;\r\nDO $$\r\nBEGIN\r\n IF NOT EXISTS (\r\n SELECT 1 FROM pg_constraint WHERE conname = 'zerobucket_storage_location_check'\r\n ) THEN\r\n ALTER TABLE zerobucket_images ADD CONSTRAINT zerobucket_storage_location_check\r\n CHECK (\r\n (storage_backend = 'postgres'\r\n AND data IS NOT NULL\r\n AND object_storage_key IS NULL\r\n AND object_storage_bucket IS NULL)\r\n OR\r\n (storage_backend = 'object_storage'\r\n AND data IS NULL\r\n AND object_storage_key IS NOT NULL\r\n AND object_storage_bucket IS NOT NULL)\r\n );\r\n END IF;\r\nEND $$;\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_storage_backend\r\n ON zerobucket_images (storage_backend) WHERE storage_backend != 'postgres';\r\n`;\r\n\r\nconst INSERT = `\r\nINSERT INTO zerobucket_images\r\n (data, mime_type, original_filename, size_bytes, width, height, checksum_sha256)\r\nVALUES ($1, $2, $3, $4, $5, $6, $7)\r\nRETURNING id;`;\r\n\r\nconst SELECT_FULL = `\r\nSELECT id, data, mime_type, original_filename, size_bytes, width, height,\r\n checksum_sha256, storage_backend, object_storage_bucket, object_storage_key\r\nFROM zerobucket_images WHERE id = $1;`;\r\n\r\nconst SELECT_MANY = `\r\nSELECT id, data, mime_type, original_filename, size_bytes, width, height,\r\n checksum_sha256, storage_backend, object_storage_bucket, object_storage_key\r\nFROM zerobucket_images WHERE id = ANY($1::uuid[]);`;\r\n\r\nconst SELECT_METADATA = `\r\nSELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256\r\nFROM zerobucket_images WHERE id = $1;`;\r\n\r\n// substring() is 1-indexed and clamps `length` at the value's end.\r\nconst SELECT_CHUNK = `SELECT substring(data FROM $1::int FOR $2::int) AS chunk FROM zerobucket_images WHERE id = $3;`;\r\nconst SELECT_STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = $1;`;\r\nconst DELETE_RETURNING = `DELETE FROM zerobucket_images WHERE id = $1 RETURNING storage_backend, object_storage_key;`;\r\nconst DELETE_MANY = `DELETE FROM zerobucket_images WHERE id = ANY($1::uuid[]) RETURNING id, storage_backend, object_storage_key;`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = $1;`;\r\n\r\n// FOR UPDATE: a per-row lock held until the transaction ends (see tierToObjectStorage).\r\nconst SELECT_FOR_TIERING = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = $1 FOR UPDATE;`;\r\nconst UPDATE_AFTER_TIERING = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = $1, object_storage_key = $2, updated_at = now() WHERE id = $3;`;\r\n\r\n// ---- dedup mode: content-addressed blobs + per-image refs (same schema as the Python package) ----\r\nconst DEDUP_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_blobs (\r\n checksum_sha256 CHAR(64) PRIMARY KEY,\r\n data BYTEA NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n ref_count INTEGER NOT NULL DEFAULT 0,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE TABLE IF NOT EXISTS zerobucket_image_refs (\r\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\r\n checksum_sha256 CHAR(64) NOT NULL REFERENCES zerobucket_blobs(checksum_sha256),\r\n original_filename TEXT,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\r\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_checksum ON zerobucket_image_refs (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_created_at ON zerobucket_image_refs (created_at);\r\n`;\r\n\r\n// $7 is how many references this call adds (1 for put; N when a batch holds N identical images).\r\n// ON CONFLICT makes the increment atomic at the row level: 20 concurrent puts of the same bytes give ref_count == 20.\r\nconst D_UPSERT_BLOB = `\r\nINSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count)\r\nVALUES ($1, $2, $3, $4, $5, $6, $7)\r\nON CONFLICT (checksum_sha256) DO UPDATE SET ref_count = zerobucket_blobs.ref_count + EXCLUDED.ref_count;`;\r\nconst D_INSERT_REF = `INSERT INTO zerobucket_image_refs (checksum_sha256, original_filename) VALUES ($1, $2) RETURNING id;`;\r\n// Aliased to the classic column names so one row mapper serves both modes.\r\nconst D_COLS = `r.id, b.data, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256,\r\n 'postgres'::text AS storage_backend, NULL::text AS object_storage_bucket, NULL::text AS object_storage_key`;\r\nconst D_FROM = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;\r\nconst D_DELETE_REF = `DELETE FROM zerobucket_image_refs WHERE id = $1 RETURNING checksum_sha256;`;\r\nconst D_DELETE_REFS = `DELETE FROM zerobucket_image_refs WHERE id = ANY($1::uuid[]) RETURNING id, checksum_sha256;`;\r\nconst D_DECREMENT = `UPDATE zerobucket_blobs SET ref_count = ref_count - $1 WHERE checksum_sha256 = $2 RETURNING ref_count;`;\r\nconst D_DELETE_EMPTY = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ANY($1::text[]) AND ref_count <= 0;`;\r\n\r\ninterface SqlSet {\r\n schema: string;\r\n selectFull: string;\r\n selectMany: string;\r\n selectMetadata: string;\r\n selectStreamInfo: string;\r\n selectChunk: string;\r\n exists: string;\r\n}\r\nconst CLASSIC_SQL: SqlSet = {\r\n schema: SCHEMA,\r\n selectFull: SELECT_FULL,\r\n selectMany: SELECT_MANY,\r\n selectMetadata: SELECT_METADATA,\r\n selectStreamInfo: SELECT_STREAM_INFO,\r\n selectChunk: SELECT_CHUNK,\r\n exists: EXISTS,\r\n};\r\nconst DEDUP_SQL: SqlSet = {\r\n schema: DEDUP_SCHEMA,\r\n selectFull: `SELECT ${D_COLS} ${D_FROM} WHERE r.id = $1;`,\r\n selectMany: `SELECT ${D_COLS} ${D_FROM} WHERE r.id = ANY($1::uuid[]);`,\r\n selectMetadata: `SELECT r.id, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256 ${D_FROM} WHERE r.id = $1;`,\r\n selectStreamInfo: `SELECT b.size_bytes, 'postgres'::text AS storage_backend, NULL::text AS object_storage_key ${D_FROM} WHERE r.id = $1;`,\r\n selectChunk: `SELECT substring(b.data FROM $1::int FOR $2::int) AS chunk ${D_FROM} WHERE r.id = $3;`,\r\n exists: `SELECT 1 FROM zerobucket_image_refs WHERE id = $1;`,\r\n};\r\n\r\nexport { isValidId };\r\n\r\n// SQLSTATEs worth retrying: transient conditions only, never constraint/syntax errors.\r\nconst RETRYABLE_SQLSTATES = new Set([\r\n \"40001\", // serialization_failure\r\n \"40P01\", // deadlock_detected\r\n \"08000\", // connection_exception\r\n \"08003\", // connection_does_not_exist\r\n \"08006\", // connection_failure\r\n \"08001\", // sqlclient_unable_to_establish_sqlconnection\r\n \"08004\", // sqlserver_rejected_establishment_of_sqlconnection\r\n \"57P01\", // admin_shutdown\r\n \"57P02\", // crash_shutdown\r\n \"57P03\", // cannot_connect_now\r\n \"53000\", // insufficient_resources\r\n \"53300\", // too_many_connections\r\n]);\r\nconst RETRYABLE_NODE_CODES = new Set([\r\n \"ECONNRESET\",\r\n \"ECONNREFUSED\",\r\n \"ETIMEDOUT\",\r\n \"EPIPE\",\r\n \"ECONNABORTED\",\r\n]);\r\nconst CONNECTION_MESSAGE_RE =\r\n /connection terminated|connection timeout|timeout exceeded when trying to connect|client has encountered a connection error/i;\r\n\r\n/** Transient (worth retrying) vs. permanent (fails identically every time). */\r\nexport function isRetryable(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const code = (exc as { code?: unknown }).code;\r\n if (typeof code === \"string\") {\r\n if (RETRYABLE_SQLSTATES.has(code) || RETRYABLE_NODE_CODES.has(code))\r\n return true;\r\n }\r\n return CONNECTION_MESSAGE_RE.test(exc.message);\r\n}\r\n\r\n/** Errors after which the pooled connection must be destroyed, not returned to the pool. */\r\nfunction isConnectionLevel(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const code = (exc as { code?: unknown }).code;\r\n if (\r\n typeof code === \"string\" &&\r\n (RETRYABLE_NODE_CODES.has(code) ||\r\n code.startsWith(\"08\") ||\r\n code.startsWith(\"57\"))\r\n ) {\r\n return true;\r\n }\r\n return CONNECTION_MESSAGE_RE.test(exc.message);\r\n}\r\n\r\nconst MAX_BACKOFF_MS = 2000;\r\n\r\nexport function backoffDelayMs(attempt: number, baseDelayMs: number): number {\r\n const exponential = baseDelayMs * 2 ** (attempt - 1);\r\n const jitter = Math.random() * baseDelayMs;\r\n return Math.min(exponential + jitter, MAX_BACKOFF_MS);\r\n}\r\n\r\nexport interface PostgresBackendOptions {\r\n /** postgresql:// connection string. Required unless `pool` is given. */\r\n connectionString?: string;\r\n /** Bring your own pg.Pool (not closed by `close()`). Pool sizing options are then ignored. */\r\n pool?: Pool;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Retries for transient errors on pooled calls. Default 3; 0 disables. */\r\n maxRetries?: number;\r\n /** Base for exponential backoff with jitter (capped at 2s). Default 100. */\r\n retryBaseDelayMs?: number;\r\n poolMinSize?: number;\r\n poolMaxSize?: number;\r\n /** Max wait for a pooled connection. Default 10000. */\r\n poolTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n /**\r\n * Enables transparent reads of tiered rows and `tierToObjectStorage()`.\r\n * Configure it with the same bucket/credentials used to tier the images.\r\n */\r\n objectStorage?: ObjectStorageLike;\r\n /**\r\n * Content-addressed mode: identical bytes are stored once (zerobucket_blobs) and each\r\n * put() adds a lightweight reference (zerobucket_image_refs) with its own id and filename.\r\n * Separate tables from classic mode, so the two can never be confused. Cannot be combined\r\n * with `objectStorage`. Same schema as the Python package's dedup mode.\r\n */\r\n dedup?: boolean;\r\n}\r\n\r\ntype Work<T> = (q: Queryable) => Promise<T>;\r\n\r\nexport class PostgresBackend implements StorageBackend {\r\n private pool: Pool | undefined;\r\n private readonly ownsPool: boolean;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly maxRetries: number;\r\n private readonly retryBaseDelayMs: number;\r\n private readonly autoMigrate: boolean;\r\n private readonly dedup: boolean;\r\n private readonly sql: SqlSet;\r\n\r\n constructor(private readonly opts: PostgresBackendOptions) {\r\n if (!opts.connectionString && !opts.pool) {\r\n throw new TypeError(\r\n \"PostgresBackend requires either `connectionString` or `pool`\",\r\n );\r\n }\r\n this.ownsPool = !opts.pool;\r\n this.pool = opts.pool;\r\n this.maxRetries = opts.maxRetries ?? 3;\r\n this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 100;\r\n this.autoMigrate = opts.autoMigrate ?? true;\r\n this.dedup = opts.dedup ?? false;\r\n if (this.dedup && opts.objectStorage) {\r\n throw new TypeError(\r\n \"objectStorage is not supported together with dedup: true (same restriction as the Python package). \" +\r\n \"Tiering moves one image's bytes, but a deduplicated blob can be shared by many images.\",\r\n );\r\n }\r\n this.sql = this.dedup ? DEDUP_SQL : CLASSIC_SQL;\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined; // allow a later attempt (e.g. the DB was briefly down)\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n if (!this.pool) {\r\n let pg: { Pool: typeof Pool };\r\n try {\r\n const mod = (await import(\"pg\")) as unknown as {\r\n default?: { Pool: typeof Pool };\r\n Pool: typeof Pool;\r\n };\r\n pg = mod.default ?? mod; // CJS package: ESM gives { default }, some interop gives it directly\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"PostgresBackend requires the 'pg' package. Install it with: npm install pg\",\r\n { cause },\r\n );\r\n }\r\n const config: PoolConfig = {\r\n connectionString: this.opts.connectionString,\r\n min: this.opts.poolMinSize ?? 1,\r\n max: this.opts.poolMaxSize ?? 5,\r\n connectionTimeoutMillis: this.opts.poolTimeoutMs ?? 10_000,\r\n };\r\n this.pool = new pg.Pool(config);\r\n // An idle client erroring (server restart, network drop) must not crash the process.\r\n this.pool.on(\"error\", () => {});\r\n }\r\n try {\r\n if (this.autoMigrate) await this.migrate();\r\n else await this.pool.query(\"SELECT 1\");\r\n } catch (exc) {\r\n if (this.ownsPool) {\r\n await this.pool.end().catch(() => {});\r\n this.pool = undefined;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Could not connect to PostgreSQL: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n }\r\n\r\n /** Create the schema if missing. Serialized across processes with an advisory lock. */\r\n async migrate(): Promise<void> {\r\n const pool = this.pool!;\r\n let client: PoolClient;\r\n try {\r\n client = await pool.connect();\r\n } catch (exc) {\r\n throw new StorageError(`Could not connect to PostgreSQL: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n try {\r\n await client.query(\"BEGIN\");\r\n await client.query(\r\n \"SELECT pg_advisory_xact_lock(hashtext('zerobucket_migrate'))\",\r\n );\r\n await client.query(this.sql.schema);\r\n await client.query(\"COMMIT\");\r\n client.release();\r\n } catch (exc) {\r\n await client.query(\"ROLLBACK\").catch(() => {});\r\n client.release(true);\r\n throw new StorageError(`Migration failed: ${msg(exc)}`, { cause: exc });\r\n }\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const pool = this.pool;\r\n this.pool = undefined;\r\n this.readyPromise = undefined;\r\n if (pool && this.ownsPool) await pool.end();\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /**\r\n * Run `work` and return its result, emitting exactly one OperationEvent.\r\n *\r\n * connection given -> run once on it, no BEGIN/COMMIT, no retry.\r\n * connection absent -> pooled; `tx` wraps the work in BEGIN/COMMIT; transient\r\n * failures retry the WHOLE closure on a fresh connection.\r\n */\r\n private async run<T>(\r\n operation: string,\r\n connection: Connection | undefined,\r\n work: Work<T>,\r\n { tx = false }: { tx?: boolean } = {},\r\n ): Promise<T> {\r\n if (connection) {\r\n if (!isQueryable(connection)) {\r\n throw new TypeError(\r\n \"PostgresBackend expects a pg client/pool client as `connection` (an object with query()), not a SQLite Database.\",\r\n );\r\n }\r\n const start = performance.now();\r\n try {\r\n const result = tx\r\n ? await this.atomic(connection, work)\r\n : await work(connection);\r\n this.emit(operation, start, true, null, 0);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), 0);\r\n throw exc;\r\n }\r\n }\r\n\r\n await this.ready();\r\n const start = performance.now();\r\n let attempt = 0;\r\n for (;;) {\r\n let client: PoolClient | undefined;\r\n let destroy = false;\r\n try {\r\n client = await this.pool!.connect();\r\n if (tx) await client.query(\"BEGIN\");\r\n const result = await work(client);\r\n if (tx) await client.query(\"COMMIT\");\r\n this.emit(operation, start, true, null, attempt);\r\n return result;\r\n } catch (exc) {\r\n if (client && tx) {\r\n try {\r\n await client.query(\"ROLLBACK\");\r\n } catch {\r\n destroy = true; // cannot trust a connection that cannot roll back\r\n }\r\n }\r\n if (isConnectionLevel(exc)) destroy = true;\r\n attempt += 1;\r\n if (attempt > this.maxRetries || !isRetryable(exc)) {\r\n this.emit(operation, start, false, msg(exc), attempt - 1);\r\n throw exc;\r\n }\r\n } finally {\r\n client?.release(destroy);\r\n }\r\n await sleep(backoffDelayMs(attempt, this.retryBaseDelayMs));\r\n }\r\n }\r\n\r\n /**\r\n * Make multi-statement work all-or-nothing on a CALLER-supplied connection.\r\n * - Caller has an open transaction -> SAVEPOINT: a failure rolls back only our work and the\r\n * caller's transaction stays usable; success joins their transaction.\r\n * - No open transaction (an autocommit pg.Client) -> open and commit our own, so a failure\r\n * can never leave half-applied state (e.g. an inflated dedup ref_count).\r\n */\r\n private async atomic<T>(conn: Queryable, work: Work<T>): Promise<T> {\r\n let ownTx = false;\r\n try {\r\n await conn.query(\"SAVEPOINT zerobucket_atomic\");\r\n } catch (exc) {\r\n if ((exc as { code?: string }).code !== \"25P01\") throw exc; // 25P01 = no active transaction\r\n ownTx = true;\r\n await conn.query(\"BEGIN\");\r\n }\r\n try {\r\n const result = await work(conn);\r\n await conn.query(\r\n ownTx ? \"COMMIT\" : \"RELEASE SAVEPOINT zerobucket_atomic\",\r\n );\r\n return result;\r\n } catch (exc) {\r\n try {\r\n await conn.query(\r\n ownTx ? \"ROLLBACK\" : \"ROLLBACK TO SAVEPOINT zerobucket_atomic\",\r\n );\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n if (this.dedup) {\r\n return await this.run(\r\n \"put\",\r\n connection,\r\n async (q) => {\r\n await q.query(D_UPSERT_BLOB, blobParams(row, 1));\r\n return String(\r\n (\r\n await q.query(D_INSERT_REF, [\r\n row.checksumSha256,\r\n row.originalFilename,\r\n ])\r\n ).rows[0].id,\r\n );\r\n },\r\n { tx: true },\r\n );\r\n }\r\n return await this.run(\"put\", connection, async (q) => {\r\n const res = await q.query(INSERT, rowParams(row));\r\n return String(res.rows[0].id);\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n if (this.dedup) {\r\n return await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n // Identical images within one batch share a blob: count them, upsert each distinct\r\n // checksum ONCE in sorted order (a global lock order, so two overlapping batches\r\n // can never deadlock on each other's blob rows), then create the refs in input order.\r\n const groups = new Map<string, { row: PreparedRow; n: number }>();\r\n for (const r of rows) {\r\n const g = groups.get(r.checksumSha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksumSha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const g = groups.get(checksum)!;\r\n await q.query(D_UPSERT_BLOB, blobParams(g.row, g.n));\r\n }\r\n const ids: string[] = [];\r\n for (const r of rows) {\r\n ids.push(\r\n String(\r\n (\r\n await q.query(D_INSERT_REF, [\r\n r.checksumSha256,\r\n r.originalFilename,\r\n ])\r\n ).rows[0].id,\r\n ),\r\n );\r\n }\r\n return ids;\r\n },\r\n { tx: true },\r\n );\r\n }\r\n // One transaction. A transient failure rolls back EVERYTHING, so a retry\r\n // from scratch cannot create duplicates. Statements are awaited one at a\r\n // time on the single connection (queuing concurrent queries on one pg\r\n // client is deprecated and breaks in pg@9), so result order matches input\r\n // order by construction.\r\n return await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n const ids: string[] = [];\r\n for (const r of rows)\r\n ids.push(String((await q.query(INSERT, rowParams(r))).rows[0].id));\r\n return ids;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectFull, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? this.hydrate(row, id) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.run(\r\n \"get_many\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMany, [valid])).rows,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return mapLimit(rows, 4, (r) => this.hydrate(r, String(r.id)));\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMetadata, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectStreamInfo, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n\r\n if (info.storage_backend === \"object_storage\") {\r\n const os = this.opts.objectStorage;\r\n if (!os) throw tieredWithoutStorage(id, info.object_storage_key);\r\n return streamFromObjectStorage(\r\n os,\r\n id,\r\n info.object_storage_key,\r\n start,\r\n end,\r\n chunkSize,\r\n );\r\n }\r\n const self = this;\r\n\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start; // 0-based\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.run(\r\n \"get_stream\",\r\n connection,\r\n async (q) =>\r\n (await q.query(self.sql.selectChunk, [offset + 1, length, id]))\r\n .rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n const chunk: Buffer = row.chunk;\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n if (this.dedup) return this.deleteDedup(id, connection);\r\n try {\r\n const row = await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => (await q.query(DELETE_RETURNING, [id])).rows[0],\r\n );\r\n if (row?.storage_backend === \"object_storage\")\r\n await this.cleanupObject([row.object_storage_key], connection);\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n if (this.dedup) return this.deleteManyDedup(valid, connection);\r\n try {\r\n const rows = await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => (await q.query(DELETE_MANY, [valid])).rows,\r\n );\r\n await this.cleanupObject(\r\n rows\r\n .filter((r) => r.storage_backend === \"object_storage\")\r\n .map((r) => r.object_storage_key as string),\r\n connection,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.run(\r\n \"exists\",\r\n connection,\r\n async (q) => ((await q.query(this.sql.exists, [id])).rowCount ?? 0) > 0,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n // ---- dedup deletes / migration ---------------------------------------\r\n\r\n private async deleteDedup(\r\n id: string,\r\n connection: Connection | undefined,\r\n ): Promise<boolean> {\r\n try {\r\n return await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => {\r\n const ref = (await q.query(D_DELETE_REF, [id])).rows[0];\r\n if (!ref) return false; // id did not exist: nothing to decrement either\r\n const checksum = ref.checksum_sha256 as string;\r\n const dec = (await q.query(D_DECREMENT, [1, checksum])).rows[0];\r\n if (dec && dec.ref_count <= 0)\r\n await q.query(D_DELETE_EMPTY, [[checksum]]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n private async deleteManyDedup(\r\n ids: string[],\r\n connection: Connection | undefined,\r\n ): Promise<string[]> {\r\n try {\r\n return await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => {\r\n const deleted = (await q.query(D_DELETE_REFS, [ids])).rows;\r\n if (deleted.length === 0) return [];\r\n // Several deleted refs may share one blob: decrement each blob by its exact count,\r\n // in sorted checksum order (global lock order, so concurrent batches cannot deadlock).\r\n const counts = new Map<string, number>();\r\n for (const r of deleted)\r\n counts.set(\r\n r.checksum_sha256,\r\n (counts.get(r.checksum_sha256) ?? 0) + 1,\r\n );\r\n const emptied: string[] = [];\r\n for (const checksum of [...counts.keys()].sort()) {\r\n const dec = (\r\n await q.query(D_DECREMENT, [counts.get(checksum), checksum])\r\n ).rows[0];\r\n if (dec && dec.ref_count <= 0) emptied.push(checksum);\r\n }\r\n if (emptied.length > 0) await q.query(D_DELETE_EMPTY, [emptied]);\r\n return deleted.map((r) => String(r.id));\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n /**\r\n * One-time, NON-DESTRUCTIVE copy of every row in the classic `zerobucket_images` table into this\r\n * dedup instance's tables, preserving every id (external references keep working) and\r\n * deduplicating identical content on the way. The classic table is never modified.\r\n *\r\n * Differences from the Python version, all deliberate: rows are copied in small batches (bounded\r\n * memory instead of loading the whole table); re-running is safe (already-migrated ids are\r\n * skipped and reported, not an error); and classic rows already tiered to object storage are\r\n * refused up front with a clear message (their bytes are not in the database to copy).\r\n * Still all-or-nothing: one transaction.\r\n */\r\n async migrateClassicToDedup(): Promise<{\r\n imagesMigrated: number;\r\n distinctBlobsCreated: number;\r\n duplicateReferencesFound: number;\r\n alreadyMigrated: number;\r\n }> {\r\n if (!this.dedup) {\r\n throw new StorageError(\r\n \"migrateClassicToDedup() must be called on an instance created with dedup: true.\",\r\n );\r\n }\r\n await this.ready();\r\n const client = await this.pool!.connect();\r\n let destroy = false;\r\n try {\r\n await client.query(\"BEGIN\");\r\n await client.query(\r\n \"SELECT pg_advisory_xact_lock(hashtext('zerobucket_migrate_dedup'))\",\r\n );\r\n if (\r\n !(await client.query(\"SELECT to_regclass('zerobucket_images') AS t\"))\r\n .rows[0].t\r\n ) {\r\n throw new StorageError(\r\n \"No classic zerobucket_images table found in this database -- nothing to migrate.\",\r\n );\r\n }\r\n const hasTierCol = (\r\n await client.query(\r\n \"SELECT 1 FROM information_schema.columns WHERE table_name = 'zerobucket_images' AND column_name = 'storage_backend'\",\r\n )\r\n ).rowCount;\r\n if (hasTierCol) {\r\n const tiered = (\r\n await client.query(\r\n \"SELECT count(*)::int AS n FROM zerobucket_images WHERE storage_backend <> 'postgres'\",\r\n )\r\n ).rows[0].n;\r\n if (tiered > 0) {\r\n throw new StorageError(\r\n `${tiered} classic image(s) are tiered to object storage, so their bytes are not in the database to migrate. ` +\r\n \"Dedup mode does not support tiering. Migrate only an un-tiered table, or bring those images back first.\",\r\n );\r\n }\r\n }\r\n\r\n const seen = new Set<string>();\r\n let migrated = 0;\r\n let already = 0;\r\n // created_at travels as TEXT, never a JS Date: Postgres keeps microseconds, a Date keeps only\r\n // milliseconds, and a truncated keyset cursor sorts before its own row (infinite loop).\r\n let cursor: { createdAt: string; id: string } | null = null;\r\n const BATCH = 20;\r\n for (;;) {\r\n const page: { rows: any[] } = cursor\r\n ? await client.query(\r\n `SELECT id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at::text AS created_at_txt\r\n FROM zerobucket_images WHERE (created_at, id) > ($1::timestamptz, $2::uuid) ORDER BY created_at, id LIMIT ${BATCH}`,\r\n [cursor.createdAt, cursor.id],\r\n )\r\n : await client.query(\r\n `SELECT id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at::text AS created_at_txt\r\n FROM zerobucket_images ORDER BY created_at, id LIMIT ${BATCH}`,\r\n );\r\n if (page.rows.length === 0) break;\r\n const last = page.rows[page.rows.length - 1];\r\n if (\r\n cursor &&\r\n cursor.createdAt === last.created_at_txt &&\r\n cursor.id === last.id\r\n ) {\r\n throw new StorageError(\r\n \"Migration made no progress (keyset cursor did not advance); aborting instead of looping.\",\r\n );\r\n }\r\n cursor = { createdAt: last.created_at_txt, id: last.id };\r\n\r\n const have = new Set(\r\n (\r\n await client.query(\r\n \"SELECT id FROM zerobucket_image_refs WHERE id = ANY($1::uuid[])\",\r\n [page.rows.map((r) => r.id)],\r\n )\r\n ).rows.map((r) => String(r.id)),\r\n );\r\n const fresh = page.rows.filter((r) => !have.has(String(r.id)));\r\n already += page.rows.length - fresh.length;\r\n\r\n const groups = new Map<string, { row: any; n: number }>();\r\n for (const r of fresh) {\r\n const g = groups.get(r.checksum_sha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksum_sha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const { row, n } = groups.get(checksum)!;\r\n await client.query(D_UPSERT_BLOB, [\r\n checksum,\r\n row.data,\r\n row.mime_type,\r\n row.size_bytes,\r\n row.width,\r\n row.height,\r\n n,\r\n ]);\r\n }\r\n for (const r of fresh) {\r\n await client.query(\r\n \"INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at) VALUES ($1, $2, $3, $4::timestamptz)\",\r\n [r.id, r.checksum_sha256, r.original_filename, r.created_at_txt],\r\n );\r\n seen.add(r.checksum_sha256);\r\n migrated += 1;\r\n }\r\n }\r\n await client.query(\"COMMIT\");\r\n return {\r\n imagesMigrated: migrated,\r\n distinctBlobsCreated: seen.size,\r\n duplicateReferencesFound: migrated - seen.size,\r\n alreadyMigrated: already,\r\n };\r\n } catch (exc) {\r\n try {\r\n await client.query(\"ROLLBACK\");\r\n } catch {\r\n destroy = true;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Migration to dedup failed: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n } finally {\r\n client.release(destroy);\r\n }\r\n }\r\n\r\n // ---- helpers ---------------------------------------------------------\r\n\r\n private hydrate(row: any, id: string): Promise<StoredRecord> {\r\n return hydrateRow(row, id, this.opts.objectStorage);\r\n }\r\n\r\n /**\r\n * Remove tiered objects AFTER their rows are gone. Ordering is deliberate:\r\n * once the row is deleted the image is correctly \"not found\" to everyone, so\r\n * a failed object delete only leaves a harmless orphan, whereas deleting the\r\n * object first and then failing the row delete would leave a row pointing at nothing.\r\n *\r\n * Never runs inside a caller-owned transaction (`connection`): we cannot know\r\n * whether it will commit, and destroying data before the commit outcome is known\r\n * could leave a rolled-back row pointing at a deleted object. In that case the\r\n * object stays until you remove it yourself after committing (objectStorage.delete(id)).\r\n * Failures are reported through onOperation and never fail the delete itself.\r\n */\r\n private async cleanupObject(\r\n keys: string[],\r\n connection: Connection | undefined,\r\n ): Promise<void> {\r\n const os = this.opts.objectStorage;\r\n if (!os || connection || keys.length === 0) return;\r\n await Promise.all(\r\n keys.map(async (key) => {\r\n const start = performance.now();\r\n try {\r\n await os.delete(key);\r\n this.emit(\"object_storage_delete\", start, true, null, 0);\r\n } catch (exc) {\r\n this.emit(\"object_storage_delete\", start, false, msg(exc), 0);\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Move an image's bytes out of Postgres into object storage, replacing the\r\n * row's `data` with a pointer. Returns:\r\n * null - no such image\r\n * false - already tiered (a safe no-op, so backfills can be re-run)\r\n * true - tiered just now\r\n *\r\n * SAFETY: the upload happens INSIDE the same transaction as the row lock\r\n * (SELECT ... FOR UPDATE) and the UPDATE that flips the row. If the upload\r\n * fails, the transaction rolls back and the row is untouched: there is no\r\n * moment when the bytes exist nowhere, or when a row claims to be tiered\r\n * without a completed upload.\r\n *\r\n * TRADEOFF: the row lock is held for the whole upload. Concurrent writes\r\n * to THIS image (delete, tier) wait; every other row is unaffected, and plain\r\n * reads are never blocked (Postgres MVCC). Meant for explicit maintenance, not hot paths.\r\n *\r\n * Retry is safe: a transient error replays the whole closure, and the\r\n * deterministic key (the image id) makes the re-upload an idempotent overwrite.\r\n */\r\n async tierToObjectStorage(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<boolean | null> {\r\n if (this.dedup) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images).\",\r\n );\r\n }\r\n const os = this.opts.objectStorage;\r\n if (!os) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() requires objectStorage to be configured (new ZeroBucket({ objectStorage: new ObjectStorage({ bucket }) })).\",\r\n );\r\n }\r\n if (!isValidId(id)) return null;\r\n const key = id.toLowerCase();\r\n try {\r\n return await this.run(\r\n \"tier_to_object_storage\",\r\n connection,\r\n async (q) => {\r\n const row = (await q.query(SELECT_FOR_TIERING, [id])).rows[0];\r\n if (!row) return null;\r\n if (row.storage_backend !== \"postgres\") return false;\r\n await os.upload(key, row.data as Buffer, { mimeType: row.mime_type });\r\n await q.query(UPDATE_AFTER_TIERING, [os.bucket, key, id]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to tier image to object storage\", exc);\r\n }\r\n }\r\n}\r\n\r\nfunction blobParams(r: PreparedRow, refs: number): unknown[] {\r\n return [\r\n r.checksumSha256,\r\n r.data,\r\n r.mimeType,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n refs,\r\n ];\r\n}\r\n\r\nfunction rowParams(r: PreparedRow): unknown[] {\r\n return [\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n ];\r\n}\r\n\r\nfunction isQueryable(c: Connection): c is Queryable {\r\n return typeof (c as Queryable).query === \"function\";\r\n}\r\n","/**\r\n * SQLite storage adapter (classic mode, dedup mode, streaming, object-storage tiering).\r\n *\r\n * Uses the EXACT schema the Python package's SQLiteBackend creates, so a Node app and a\r\n * Python app can share one `.db` file: ids are TEXT UUIDs generated by the application, timestamps are\r\n * ISO-8601 TEXT, classic rows say storage_backend='sqlite', and tiered rows point at object storage.\r\n *\r\n * HOW THIS DIFFERS FROM THE POSTGRES ADAPTER (and why)\r\n *\r\n * - The driver (`better-sqlite3`) is SYNCHRONOUS. Every operation here is async only in signature: the\r\n * SQLite call itself runs on the event loop and returns almost immediately (it is a local file).\r\n * Because a blocked synchronous call would freeze the whole process, lock contention is never waited\r\n * out inside the driver. Each attempt uses a ~25 ms busy timeout and BUSY errors are retried here with\r\n * ASYNC sleeps, up to `busyTimeoutMs` in total, so the event loop stays free.\r\n * - One long-lived connection serves all normal operations. Each unit of work runs to completion\r\n * synchronously inside BEGIN IMMEDIATE ... COMMIT, so concurrent requests can never interleave inside\r\n * a transaction.\r\n * - Tiering awaits a network upload while holding a write transaction, so it uses its OWN dedicated\r\n * connection (otherwise other requests would run inside that open transaction and a rollback would\r\n * erase their writes). SQLite has no per-row locks: BEGIN IMMEDIATE locks the WHOLE DATABASE for the\r\n * duration of the upload. Writes from other PROCESSES wait (and time out after busyTimeoutMs); writes\r\n * from this process queue behind an in-process async lock. Reads are never blocked (WAL).\r\n * - WAL mode is a durable file-level setting. It is set at most once per backend instance, and only if\r\n * the file is not already in WAL mode (re-issuing it on every connection caused real lock contention\r\n * on Windows in the Python package).\r\n * - Streaming reads use ranged `substr(data, offset, length)` queries (better-sqlite3 has no incremental\r\n * blob handle). A row deleted mid-stream therefore raises StorageError, exactly like Postgres. (The\r\n * Python SYNC SQLite adapter instead keeps streaming from a WAL snapshot; pass your own `connection`\r\n * inside a transaction here if you need that.) SQLite still reads the whole blob per chunk, in native\r\n * memory: this bounds Node-side memory only.\r\n */\r\nimport { randomUUID } from \"node:crypto\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type {\r\n Connection,\r\n OperationEvent,\r\n PreparedRow,\r\n SqliteDatabaseLike,\r\n} from \"../types.js\";\r\nimport type {\r\n CallOptions,\r\n StorageBackend,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\nimport {\r\n hydrateRow,\r\n isValidId,\r\n mapLimit,\r\n msg,\r\n sleep,\r\n streamFromObjectStorage,\r\n tieredWithoutStorage,\r\n wrap,\r\n} from \"./shared.js\";\r\n\r\n// ---- schema (identical to the Python package) -------------------------------------------------\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id TEXT PRIMARY KEY,\r\n data BLOB,\r\n mime_type TEXT NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n checksum_sha256 TEXT NOT NULL,\r\n created_at TEXT NOT NULL,\r\n updated_at TEXT NOT NULL,\r\n storage_backend TEXT NOT NULL DEFAULT 'sqlite',\r\n object_storage_bucket TEXT,\r\n object_storage_key TEXT,\r\n CHECK (\r\n (storage_backend = 'sqlite'\r\n AND data IS NOT NULL\r\n AND object_storage_key IS NULL\r\n AND object_storage_bucket IS NULL)\r\n OR\r\n (storage_backend = 'object_storage'\r\n AND data IS NULL\r\n AND object_storage_key IS NOT NULL\r\n AND object_storage_bucket IS NOT NULL)\r\n )\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_checksum ON zerobucket_images (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_created_at ON zerobucket_images (created_at);\r\n`;\r\n\r\nconst DEDUP_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_blobs (\r\n checksum_sha256 TEXT PRIMARY KEY,\r\n data BLOB NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n ref_count INTEGER NOT NULL DEFAULT 0,\r\n created_at TEXT NOT NULL\r\n);\r\nCREATE TABLE IF NOT EXISTS zerobucket_image_refs (\r\n id TEXT PRIMARY KEY,\r\n checksum_sha256 TEXT NOT NULL REFERENCES zerobucket_blobs(checksum_sha256),\r\n original_filename TEXT,\r\n created_at TEXT NOT NULL,\r\n updated_at TEXT NOT NULL\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_checksum ON zerobucket_image_refs (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_created_at ON zerobucket_image_refs (created_at);\r\n`;\r\n\r\n// ---- SQL --------------------------------------------------------------------------------------\r\n\r\nconst COLS = `id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, storage_backend, object_storage_bucket, object_storage_key`;\r\nconst INSERT = `INSERT INTO zerobucket_images (id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\r\nconst SELECT_FULL = `SELECT ${COLS} FROM zerobucket_images WHERE id = ?`;\r\nconst selectMany = (n: number) =>\r\n `SELECT ${COLS} FROM zerobucket_images WHERE id IN (${placeholders(n)})`;\r\nconst SELECT_METADATA = `SELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256 FROM zerobucket_images WHERE id = ?`;\r\nconst STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = ?`;\r\nconst CHUNK = `SELECT substr(data, ?, ?) AS chunk FROM zerobucket_images WHERE id = ?`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = ?`;\r\nconst DELETE_ONE = `DELETE FROM zerobucket_images WHERE id = ? RETURNING storage_backend, object_storage_key`;\r\nconst deleteMany = (n: number) =>\r\n `DELETE FROM zerobucket_images WHERE id IN (${placeholders(n)}) RETURNING id, storage_backend, object_storage_key`;\r\nconst SELECT_FOR_TIERING = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = ?`;\r\nconst UPDATE_AFTER_TIERING = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = ?, object_storage_key = ?, updated_at = ? WHERE id = ?`;\r\n\r\n// dedup. Reads are aliased to the classic column names so one row mapper serves both modes.\r\nconst D_FROM = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;\r\nconst D_COLS = `r.id AS id, b.data AS data, b.mime_type AS mime_type, r.original_filename AS original_filename, b.size_bytes AS size_bytes, b.width AS width, b.height AS height, r.checksum_sha256 AS checksum_sha256, 'sqlite' AS storage_backend, NULL AS object_storage_bucket, NULL AS object_storage_key`;\r\nconst D_UPSERT = `INSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (checksum_sha256) DO UPDATE SET ref_count = ref_count + excluded.ref_count`;\r\nconst D_INSERT_REF = `INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`;\r\nconst D_SELECT_FULL = `SELECT ${D_COLS} ${D_FROM} WHERE r.id = ?`;\r\nconst dSelectMany = (n: number) =>\r\n `SELECT ${D_COLS} ${D_FROM} WHERE r.id IN (${placeholders(n)})`;\r\nconst D_SELECT_METADATA = `SELECT r.id AS id, b.mime_type AS mime_type, r.original_filename AS original_filename, b.size_bytes AS size_bytes, b.width AS width, b.height AS height, r.checksum_sha256 AS checksum_sha256 ${D_FROM} WHERE r.id = ?`;\r\nconst D_STREAM_INFO = `SELECT b.size_bytes AS size_bytes, 'sqlite' AS storage_backend, NULL AS object_storage_key ${D_FROM} WHERE r.id = ?`;\r\nconst D_CHUNK = `SELECT substr(b.data, ?, ?) AS chunk ${D_FROM} WHERE r.id = ?`;\r\nconst D_EXISTS = `SELECT 1 FROM zerobucket_image_refs WHERE id = ?`;\r\nconst D_DELETE_REF = `DELETE FROM zerobucket_image_refs WHERE id = ? RETURNING checksum_sha256`;\r\nconst dDeleteRefs = (n: number) =>\r\n `DELETE FROM zerobucket_image_refs WHERE id IN (${placeholders(n)}) RETURNING id, checksum_sha256`;\r\nconst D_DECREMENT = `UPDATE zerobucket_blobs SET ref_count = ref_count - ? WHERE checksum_sha256 = ? RETURNING ref_count`;\r\nconst D_DELETE_EMPTY = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ? AND ref_count <= 0`;\r\n\r\n/** SQLite's default max bound variables is 999 on old builds; stay well under it. */\r\nconst IN_CHUNK = 500;\r\n/** Per-attempt busy wait inside the (synchronous) driver. Longer waits are done by async retry, never by blocking the loop. */\r\nconst ATTEMPT_BUSY_MS = 25;\r\nexport const DEFAULT_BUSY_TIMEOUT_MS = 10_000;\r\n\r\nfunction placeholders(n: number): string {\r\n return Array.from({ length: n }, () => \"?\").join(\", \");\r\n}\r\n\r\n/** Python-compatible ISO-8601 UTC with microseconds: 2026-10-03T12:00:00.123000+00:00 */\r\nfunction isoNow(): string {\r\n return new Date().toISOString().replace(\"Z\", \"000+00:00\");\r\n}\r\n\r\nfunction isBusy(exc: unknown): boolean {\r\n const code = (exc as { code?: unknown })?.code;\r\n return (\r\n typeof code === \"string\" &&\r\n (code.startsWith(\"SQLITE_BUSY\") || code.startsWith(\"SQLITE_LOCKED\"))\r\n );\r\n}\r\n\r\n/** The members of better-sqlite3's Database that this adapter uses beyond SqliteDatabaseLike. */\r\ninterface Db extends SqliteDatabaseLike {\r\n pragma(source: string, options?: { simple?: boolean }): unknown;\r\n close(): void;\r\n}\r\ntype DbCtor = new (path: string, options?: { timeout?: number }) => Db;\r\n\r\n/** Tiny async mutex: serializes in-process writers (and tiering) without blocking the event loop. */\r\nclass Mutex {\r\n private tail: Promise<void> = Promise.resolve();\r\n run<T>(fn: () => Promise<T>): Promise<T> {\r\n const prev = this.tail;\r\n let release!: () => void;\r\n this.tail = new Promise<void>((r) => (release = r));\r\n return prev.then(fn).finally(release);\r\n }\r\n}\r\n\r\nexport interface SQLiteBackendOptions {\r\n /** Database file path, or \":memory:\" for a throwaway in-process database (tiering is unavailable for \":memory:\"). */\r\n path: string;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Content-addressed mode (separate tables; same schema as the Python package). Cannot be combined with objectStorage. */\r\n dedup?: boolean;\r\n /** Enables tiering and transparent reads of tiered rows. */\r\n objectStorage?: ObjectStorageLike;\r\n /** Total time to keep retrying when another connection/process holds the write lock. Default 10000. */\r\n busyTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n}\r\n\r\nexport class SQLiteBackend implements StorageBackend {\r\n private db: Db | undefined;\r\n private Database: DbCtor | undefined;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly dedup: boolean;\r\n private readonly busyTimeoutMs: number;\r\n private readonly writeLock = new Mutex();\r\n\r\n constructor(private readonly opts: SQLiteBackendOptions) {\r\n if (!opts.path)\r\n throw new TypeError(\r\n 'SQLiteBackend requires a `path` (a file path or \":memory:\")',\r\n );\r\n this.dedup = opts.dedup ?? false;\r\n this.busyTimeoutMs = opts.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;\r\n if (this.dedup && opts.objectStorage) {\r\n throw new TypeError(\r\n \"objectStorage is not supported together with dedup: true (same restriction as the Python package). \" +\r\n \"Tiering moves one image's bytes, but a deduplicated blob can be shared by many images.\",\r\n );\r\n }\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined;\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n try {\r\n const mod = (await import(\"better-sqlite3\")) as unknown as {\r\n default?: DbCtor;\r\n } & DbCtor;\r\n this.Database = mod.default ?? mod;\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"SQLiteBackend requires the 'better-sqlite3' package. Install it with: npm install better-sqlite3\",\r\n { cause },\r\n );\r\n }\r\n const db = new this.Database(this.opts.path, { timeout: ATTEMPT_BUSY_MS });\r\n try {\r\n await this.retrying(() => {\r\n // WAL is durable and file-level: only convert if the file is not already WAL (avoids needless\r\n // lock contention when many processes open the same fresh file at once).\r\n if (\r\n this.opts.path !== \":memory:\" &&\r\n db.pragma(\"journal_mode\", { simple: true }) !== \"wal\"\r\n ) {\r\n db.pragma(\"journal_mode = WAL\");\r\n }\r\n db.pragma(\"foreign_keys = ON\");\r\n if (this.opts.autoMigrate ?? true)\r\n db.exec(this.dedup ? DEDUP_SCHEMA : SCHEMA);\r\n });\r\n } catch (exc) {\r\n try {\r\n db.close();\r\n } catch {\r\n // ignore\r\n }\r\n throw new StorageError(\r\n `Could not open SQLite database ${JSON.stringify(this.opts.path)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n this.db = db;\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const db = this.db;\r\n this.db = undefined;\r\n this.readyPromise = undefined;\r\n // let any in-flight write finish before the handle goes away\r\n await this.writeLock.run(async () => {\r\n db?.close();\r\n });\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /** Re-run `fn` while another connection holds a lock, sleeping asynchronously between attempts. `fn` must be atomic. */\r\n private async retrying<T>(\r\n fn: () => T,\r\n onRetry?: (n: number) => void,\r\n ): Promise<T> {\r\n const deadline = performance.now() + this.busyTimeoutMs;\r\n let attempt = 0;\r\n for (;;) {\r\n try {\r\n return fn();\r\n } catch (exc) {\r\n if (!isBusy(exc) || performance.now() >= deadline) throw exc;\r\n attempt += 1;\r\n onRetry?.(attempt);\r\n await sleep(\r\n Math.min(5 * 2 ** Math.min(attempt, 5), 100) + Math.random() * 5,\r\n );\r\n }\r\n }\r\n }\r\n\r\n /** BEGIN IMMEDIATE takes the write lock up front, so a BUSY comes before any work and the whole unit is safely re-runnable. */\r\n private tx<T>(db: Db, work: (db: Db) => T): T {\r\n db.exec(\"BEGIN IMMEDIATE\");\r\n try {\r\n const result = work(db);\r\n db.exec(\"COMMIT\");\r\n return result;\r\n } catch (exc) {\r\n try {\r\n db.exec(\"ROLLBACK\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n /**\r\n * All-or-nothing work on a CALLER-supplied handle. SAVEPOINT works whether or not the caller has an open\r\n * transaction: inside one it nests (a failure rolls back only our work); outside one it begins and, on\r\n * RELEASE, commits.\r\n */\r\n private atomic<T>(db: Db, work: (db: Db) => T): T {\r\n db.exec(\"SAVEPOINT zerobucket_atomic\");\r\n try {\r\n const result = work(db);\r\n db.exec(\"RELEASE SAVEPOINT zerobucket_atomic\");\r\n return result;\r\n } catch (exc) {\r\n try {\r\n db.exec(\"ROLLBACK TO SAVEPOINT zerobucket_atomic\");\r\n db.exec(\"RELEASE SAVEPOINT zerobucket_atomic\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n private async execute<T>(\r\n operation: string,\r\n connection: Connection | undefined,\r\n mode: \"read\" | \"write\",\r\n work: (db: Db) => T,\r\n ): Promise<T> {\r\n const start = performance.now();\r\n let retries = 0;\r\n try {\r\n let result: T;\r\n if (connection) {\r\n if (typeof (connection as SqliteDatabaseLike).prepare !== \"function\") {\r\n throw new TypeError(\r\n \"SQLiteBackend expects a better-sqlite3 Database as `connection` (an object with prepare()), not a pg client.\",\r\n );\r\n }\r\n const db = connection as Db;\r\n result = mode === \"write\" ? this.atomic(db, work) : work(db);\r\n } else {\r\n await this.ready();\r\n const db = this.db!;\r\n const onRetry = (n: number) => (retries = n);\r\n result =\r\n mode === \"write\"\r\n ? await this.writeLock.run(() =>\r\n this.retrying(() => this.tx(db, work), onRetry),\r\n )\r\n : await this.retrying(() => work(db), onRetry);\r\n }\r\n this.emit(operation, start, true, null, retries);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), retries);\r\n throw exc;\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n return await this.execute(\"put\", connection, \"write\", (db) =>\r\n this.insertOne(db, row),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n private insertOne(db: Db, r: PreparedRow): string {\r\n const id = randomUUID();\r\n const now = isoNow();\r\n if (this.dedup) {\r\n db.prepare(D_UPSERT).run(\r\n r.checksumSha256,\r\n r.data,\r\n r.mimeType,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n 1,\r\n now,\r\n );\r\n db.prepare(D_INSERT_REF).run(\r\n id,\r\n r.checksumSha256,\r\n r.originalFilename,\r\n now,\r\n now,\r\n );\r\n } else {\r\n db.prepare(INSERT).run(\r\n id,\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n now,\r\n now,\r\n );\r\n }\r\n return id;\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n return await this.execute(\"put_many\", connection, \"write\", (db) => {\r\n if (!this.dedup) return rows.map((r) => this.insertOne(db, r));\r\n // Identical images in one batch share a blob: count them and upsert each distinct checksum once.\r\n const now = isoNow();\r\n const groups = new Map<string, { row: PreparedRow; n: number }>();\r\n for (const r of rows) {\r\n const g = groups.get(r.checksumSha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksumSha256, { row: r, n: 1 });\r\n }\r\n for (const [checksum, { row, n }] of groups) {\r\n db.prepare(D_UPSERT).run(\r\n checksum,\r\n row.data,\r\n row.mimeType,\r\n row.sizeBytes,\r\n row.width,\r\n row.height,\r\n n,\r\n now,\r\n );\r\n }\r\n return rows.map((r) => {\r\n const id = randomUUID();\r\n db.prepare(D_INSERT_REF).run(\r\n id,\r\n r.checksumSha256,\r\n r.originalFilename,\r\n now,\r\n now,\r\n );\r\n return id;\r\n });\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n let row: any;\r\n try {\r\n row = await this.execute(\"get\", connection, \"read\", (db) =>\r\n db.prepare(this.dedup ? D_SELECT_FULL : SELECT_FULL).get(norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? hydrateRow(row, norm, this.opts.objectStorage) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = [\r\n ...new Set(ids.filter(isValidId).map((i) => i.toLowerCase())),\r\n ];\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.execute(\"get_many\", connection, \"read\", (db) => {\r\n const out: any[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n out.push(\r\n ...db\r\n .prepare(\r\n this.dedup ? dSelectMany(part.length) : selectMany(part.length),\r\n )\r\n .all(...part),\r\n );\r\n }\r\n return out;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return mapLimit(rows, 4, (r) =>\r\n hydrateRow(r, String(r.id), this.opts.objectStorage),\r\n );\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.execute(\"get_metadata\", connection, \"read\", (db) =>\r\n db\r\n .prepare(this.dedup ? D_SELECT_METADATA : SELECT_METADATA)\r\n .get(id.toLowerCase()),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.execute(\"get_metadata\", connection, \"read\", (db) =>\r\n db.prepare(this.dedup ? D_STREAM_INFO : STREAM_INFO).get(norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n\r\n if (info.storage_backend === \"object_storage\") {\r\n const os = this.opts.objectStorage;\r\n if (!os) throw tieredWithoutStorage(norm, info.object_storage_key);\r\n return streamFromObjectStorage(\r\n os,\r\n norm,\r\n info.object_storage_key,\r\n start,\r\n end,\r\n chunkSize,\r\n );\r\n }\r\n\r\n const self = this;\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start;\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.execute(\"get_stream\", connection, \"read\", (db) =>\r\n db\r\n .prepare(self.dedup ? D_CHUNK : CHUNK)\r\n .get(offset + 1, length, norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(norm)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n const chunk: Buffer | null = row.chunk;\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(norm)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n const norm = id.toLowerCase();\r\n try {\r\n if (this.dedup) {\r\n return await this.execute(\"delete\", connection, \"write\", (db) => {\r\n const ref = db.prepare(D_DELETE_REF).get(norm);\r\n if (!ref) return false;\r\n const dec = db.prepare(D_DECREMENT).get(1, ref.checksum_sha256);\r\n if (dec && dec.ref_count <= 0)\r\n db.prepare(D_DELETE_EMPTY).run(ref.checksum_sha256);\r\n return true;\r\n });\r\n }\r\n const row = await this.execute(\"delete\", connection, \"write\", (db) =>\r\n db.prepare(DELETE_ONE).get(norm),\r\n );\r\n if (row?.storage_backend === \"object_storage\")\r\n await this.cleanupObjects([row.object_storage_key], connection);\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = [\r\n ...new Set(ids.filter(isValidId).map((i) => i.toLowerCase())),\r\n ];\r\n if (valid.length === 0) return [];\r\n try {\r\n if (this.dedup) {\r\n return await this.execute(\"delete_many\", connection, \"write\", (db) => {\r\n const deleted: { id: string; checksum_sha256: string }[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n deleted.push(...db.prepare(dDeleteRefs(part.length)).all(...part));\r\n }\r\n const counts = new Map<string, number>();\r\n for (const r of deleted)\r\n counts.set(\r\n r.checksum_sha256,\r\n (counts.get(r.checksum_sha256) ?? 0) + 1,\r\n );\r\n for (const [checksum, n] of counts) {\r\n const dec = db.prepare(D_DECREMENT).get(n, checksum);\r\n if (dec && dec.ref_count <= 0)\r\n db.prepare(D_DELETE_EMPTY).run(checksum);\r\n }\r\n return deleted.map((r) => String(r.id));\r\n });\r\n }\r\n const rows = await this.execute(\r\n \"delete_many\",\r\n connection,\r\n \"write\",\r\n (db) => {\r\n const out: any[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n out.push(...db.prepare(deleteMany(part.length)).all(...part));\r\n }\r\n return out;\r\n },\r\n );\r\n await this.cleanupObjects(\r\n rows\r\n .filter((r) => r.storage_backend === \"object_storage\")\r\n .map((r) => r.object_storage_key as string),\r\n connection,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.execute(\r\n \"exists\",\r\n connection,\r\n \"read\",\r\n (db) =>\r\n db.prepare(this.dedup ? D_EXISTS : EXISTS).get(id.toLowerCase()) !==\r\n undefined,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n /**\r\n * Remove tiered objects AFTER their rows are gone (a failed object delete leaves a harmless orphan; the\r\n * reverse order could leave a row pointing at nothing). Skipped inside a caller-owned transaction, since the\r\n * commit outcome is unknown. Failures never fail the delete; they are reported through onOperation.\r\n */\r\n private async cleanupObjects(\r\n keys: string[],\r\n connection: Connection | undefined,\r\n ): Promise<void> {\r\n const os = this.opts.objectStorage;\r\n if (!os || connection || keys.length === 0) return;\r\n await Promise.all(\r\n keys.map(async (key) => {\r\n const start = performance.now();\r\n try {\r\n await os.delete(key);\r\n this.emit(\"object_storage_delete\", start, true, null, 0);\r\n } catch (exc) {\r\n this.emit(\"object_storage_delete\", start, false, msg(exc), 0);\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Move an image's bytes into object storage. null = no such image, false = already tiered, true = tiered now.\r\n *\r\n * SAFETY (same guarantee as Postgres): the upload happens INSIDE a BEGIN IMMEDIATE transaction; if it fails the\r\n * transaction rolls back and the row is untouched.\r\n *\r\n * TRADEOFF (coarser than Postgres): SQLite has no row locks, so BEGIN IMMEDIATE locks the WHOLE DATABASE for the\r\n * upload. Other processes' writes wait; this process's writes queue behind an async lock; reads are not blocked.\r\n */\r\n async tierToObjectStorage(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<boolean | null> {\r\n if (this.dedup)\r\n throw new StorageError(\r\n \"tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images).\",\r\n );\r\n const os = this.opts.objectStorage;\r\n if (!os) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() requires objectStorage to be configured (new ZeroBucket({ sqlite, objectStorage: new ObjectStorage({ bucket }) })).\",\r\n );\r\n }\r\n if (connection) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() does not accept `connection` on SQLite: it needs its own dedicated connection for the BEGIN IMMEDIATE transaction.\",\r\n );\r\n }\r\n if (this.opts.path === \":memory:\") {\r\n throw new StorageError(\r\n \"tierToObjectStorage() needs a database file: an in-memory SQLite database cannot be shared with its dedicated tiering connection.\",\r\n );\r\n }\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n const start = performance.now();\r\n let retries = 0;\r\n try {\r\n await this.ready();\r\n const result = await this.writeLock.run(async () => {\r\n const tdb = new this.Database!(this.opts.path, {\r\n timeout: ATTEMPT_BUSY_MS,\r\n });\r\n try {\r\n await this.retrying(\r\n () => tdb.exec(\"BEGIN IMMEDIATE\"),\r\n (n) => (retries = n),\r\n );\r\n try {\r\n const row = tdb.prepare(SELECT_FOR_TIERING).get(norm);\r\n if (!row) {\r\n tdb.exec(\"ROLLBACK\");\r\n return null;\r\n }\r\n if (row.storage_backend !== \"sqlite\") {\r\n tdb.exec(\"ROLLBACK\");\r\n return false;\r\n }\r\n await os.upload(norm, row.data as Buffer, {\r\n mimeType: row.mime_type,\r\n });\r\n tdb\r\n .prepare(UPDATE_AFTER_TIERING)\r\n .run(os.bucket, norm, isoNow(), norm);\r\n tdb.exec(\"COMMIT\");\r\n return true;\r\n } catch (exc) {\r\n try {\r\n tdb.exec(\"ROLLBACK\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n } finally {\r\n tdb.close();\r\n }\r\n });\r\n this.emit(\"tier_to_object_storage\", start, true, null, retries);\r\n return result;\r\n } catch (exc) {\r\n this.emit(\"tier_to_object_storage\", start, false, msg(exc), retries);\r\n throw wrap(\"Failed to tier image to object storage\", exc);\r\n }\r\n }\r\n}\r\n","/**\r\n * Image validation. Never trusts file extensions or client-supplied MIME types:\r\n * the bytes are decoded with sharp (libvips) and the *detected* format is what\r\n * gets validated and stored.\r\n *\r\n * sharp is an optional peer dependency, imported lazily, so `import \"zerobucket\"`\r\n * works without it (e.g. when you only store PDFs through a custom validator).\r\n */\r\nimport {\r\n CorruptedImageError,\r\n ImageTooLargeError,\r\n ImageValidationError,\r\n StorageError,\r\n UnsupportedFormatError,\r\n} from \"./errors.js\";\r\n\r\n/** sharp format name -> canonical MIME type. Deliberately small allowlist. */\r\nconst FORMAT_TO_MIME: Record<string, string> = {\r\n jpeg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n};\r\n\r\nexport const SUPPORTED_FORMATS: readonly string[] = Object.keys(FORMAT_TO_MIME);\r\n\r\n/** Reject images that would decode to more pixels than this, regardless of compressed size (~89 MP). */\r\nexport const DEFAULT_MAX_PIXELS = 89_000_000;\r\n\r\nexport interface ValidatedImage {\r\n mimeType: string;\r\n width: number;\r\n height: number;\r\n sizeBytes: number;\r\n}\r\n\r\ntype Sharp = typeof import(\"sharp\");\r\nlet sharpPromise: Promise<Sharp> | undefined;\r\n\r\n/** Lazily load sharp, with an actionable error if it is not installed. */\r\nexport function loadSharp(): Promise<Sharp> {\r\n sharpPromise ??= import(\"sharp\").then(\r\n // ESM gives { default: sharp }; some CJS interop gives sharp directly.\r\n (m) =>\r\n (m as unknown as { default?: Sharp }).default ?? (m as unknown as Sharp),\r\n (cause) => {\r\n sharpPromise = undefined;\r\n throw new StorageError(\r\n \"Image validation requires the optional dependency 'sharp'. Install it with: npm install sharp\",\r\n { cause },\r\n );\r\n },\r\n );\r\n return sharpPromise;\r\n}\r\n\r\nexport async function validateImage(\r\n data: Buffer,\r\n opts: {\r\n maxBytes: number;\r\n maxPixels?: number;\r\n allowedFormats?: readonly string[];\r\n },\r\n): Promise<ValidatedImage> {\r\n const {\r\n maxBytes,\r\n maxPixels = DEFAULT_MAX_PIXELS,\r\n allowedFormats = SUPPORTED_FORMATS,\r\n } = opts;\r\n const sizeBytes = data.length;\r\n if (sizeBytes > maxBytes) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n if (sizeBytes === 0) throw new CorruptedImageError(\"Image data is empty\");\r\n\r\n const sharp = await loadSharp();\r\n // failOn \"error\": truncated / corrupt bodies throw instead of silently decoding partially.\r\n const make = () =>\r\n sharp(data, { limitInputPixels: maxPixels, failOn: \"error\" });\r\n\r\n let format: string | undefined;\r\n let width: number | undefined;\r\n let height: number | undefined;\r\n try {\r\n const meta = await make().metadata();\r\n format = meta.format;\r\n width = meta.width;\r\n height = meta.height;\r\n } catch (exc) {\r\n if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n throw new CorruptedImageError(\r\n `Could not decode image: ${errMessage(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n\r\n if (\r\n !format ||\r\n !allowedFormats.includes(format) ||\r\n !(format in FORMAT_TO_MIME)\r\n ) {\r\n throw new UnsupportedFormatError(format ?? null, allowedFormats);\r\n }\r\n if (!width || !height)\r\n throw new CorruptedImageError(\"Image has no readable dimensions\");\r\n\r\n // Header parsing is not enough: force a full pixel decode to catch truncated bodies.\r\n try {\r\n await make().stats();\r\n } catch (exc) {\r\n if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n throw new CorruptedImageError(\r\n `Image data is truncated or corrupted: ${errMessage(exc)}`,\r\n {\r\n cause: exc,\r\n },\r\n );\r\n }\r\n\r\n return { mimeType: FORMAT_TO_MIME[format]!, width, height, sizeBytes };\r\n}\r\n\r\nfunction isPixelLimit(exc: unknown): boolean {\r\n return /pixel limit/i.test(errMessage(exc));\r\n}\r\n\r\nexport function errMessage(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n\r\nexport { ImageValidationError };\r\n","/**\r\n * Optimize pipeline: strip metadata, optionally downscale and re-encode.\r\n * Mirrors the Python package's optimize_image(), implemented on sharp.\r\n *\r\n * Differences from the Python/Pillow version, both deliberate:\r\n * - EXIF orientation is applied (auto-rotate) BEFORE metadata is stripped.\r\n * Stripping EXIF without applying it would leave phone photos sideways.\r\n * - Transparent PNG/WebP converted to JPEG is flattened onto white, not black.\r\n */\r\nimport { ImageValidationError } from \"./errors.js\";\r\nimport {\r\n DEFAULT_MAX_PIXELS,\r\n SUPPORTED_FORMATS,\r\n loadSharp,\r\n validateImage,\r\n} from \"./validation.js\";\r\n\r\nexport const DEFAULT_JPEG_QUALITY = 90;\r\nexport const DEFAULT_WEBP_QUALITY = 88;\r\n\r\nexport interface OptimizeOptions {\r\n /** Downscale (aspect ratio preserved, Lanczos) if wider than this. Never upscales. */\r\n maxWidth?: number;\r\n /** \"jpeg\" | \"png\" | \"webp\". Defaults to the source format. */\r\n format?: string;\r\n /** 1-100, JPEG/WebP only (ignored for PNG). */\r\n quality?: number;\r\n}\r\n\r\nexport interface OptimizationResult {\r\n data: Buffer;\r\n mimeType: string;\r\n width: number;\r\n height: number;\r\n sizeBytes: number;\r\n originalSizeBytes: number;\r\n}\r\n\r\nconst FORMAT_ALIASES: Record<string, string> = { jpg: \"jpeg\" };\r\n\r\nexport async function optimizeImage(\r\n data: Buffer,\r\n opts: OptimizeOptions & { maxBytes: number; maxPixels?: number },\r\n): Promise<OptimizationResult> {\r\n const { maxWidth, quality, maxBytes, maxPixels = DEFAULT_MAX_PIXELS } = opts;\r\n const sharp = await loadSharp();\r\n const originalSizeBytes = data.length;\r\n\r\n const base = sharp(data, { limitInputPixels: maxPixels, failOn: \"error\" });\r\n const meta = await base.metadata();\r\n const requested = (opts.format ?? meta.format ?? \"jpeg\").toLowerCase();\r\n const target = FORMAT_ALIASES[requested] ?? requested;\r\n if (!SUPPORTED_FORMATS.includes(target)) {\r\n throw new ImageValidationError(\r\n `Cannot re-encode to unsupported format ${JSON.stringify(target)}. Supported: ${JSON.stringify(\r\n [...SUPPORTED_FORMATS].sort(),\r\n )}`,\r\n );\r\n }\r\n\r\n // .rotate() with no args = auto-orient from EXIF. sharp drops all metadata on output by default.\r\n let pipeline = sharp(data, {\r\n limitInputPixels: maxPixels,\r\n failOn: \"error\",\r\n }).rotate();\r\n if (maxWidth !== undefined) {\r\n pipeline = pipeline.resize({\r\n width: maxWidth,\r\n fit: \"inside\",\r\n withoutEnlargement: true,\r\n kernel: \"lanczos3\",\r\n });\r\n }\r\n\r\n if (target === \"jpeg\") {\r\n pipeline = pipeline\r\n .flatten({ background: \"#ffffff\" })\r\n .jpeg({\r\n quality: quality ?? DEFAULT_JPEG_QUALITY,\r\n mozjpeg: true,\r\n progressive: true,\r\n });\r\n } else if (target === \"webp\") {\r\n pipeline = pipeline.webp({\r\n quality: quality ?? DEFAULT_WEBP_QUALITY,\r\n effort: 6,\r\n });\r\n } else {\r\n pipeline = pipeline.png({ compressionLevel: 9 }); // no lossy knob; quality ignored by design\r\n }\r\n\r\n const out = await pipeline.toBuffer();\r\n\r\n // Defense in depth: re-validate our own output before it is trusted.\r\n const revalidated = await validateImage(out, { maxBytes, maxPixels });\r\n return {\r\n data: out,\r\n mimeType: revalidated.mimeType,\r\n width: revalidated.width,\r\n height: revalidated.height,\r\n sizeBytes: revalidated.sizeBytes,\r\n originalSizeBytes,\r\n };\r\n}\r\n","/**\r\n * S3-compatible object storage for tiering (AWS S3, MinIO, Cloudflare R2,\r\n * Backblaze B2, DigitalOcean Spaces, ...).\r\n *\r\n * Scoped to exactly what tiering needs: upload, download, ranged download,\r\n * idempotent delete, exists. The bucket must already exist: bucket creation\r\n * involves choices (region, versioning, lifecycle) that belong to you, not to\r\n * a storage library.\r\n *\r\n * `@aws-sdk/client-s3` is an optional peer dependency, imported lazily on first\r\n * use, so `import \"zerobucket\"` works without it.\r\n *\r\n * Objects are keyed by the image's UUID, with no prefix. That is the same key\r\n * scheme the Python package uses, so either package can read, tier and clean\r\n * up objects the other one created in the same bucket.\r\n */\r\nimport { StorageError } from \"./errors.js\";\r\n\r\n/** The surface the Postgres adapter needs. Implement this to plug in a non-S3 store (or a test double). */\r\nexport interface ObjectStorageLike {\r\n readonly bucket: string;\r\n upload(key: string, data: Buffer, opts: { mimeType: string }): Promise<void>;\r\n download(key: string): Promise<Buffer>;\r\n /** Inclusive byte range, fetched with a real HTTP Range request. */\r\n downloadRange(\r\n key: string,\r\n start: number,\r\n endInclusive: number,\r\n ): Promise<Buffer>;\r\n /** Idempotent: deleting a missing key is not an error. */\r\n delete(key: string): Promise<void>;\r\n exists(key: string): Promise<boolean>;\r\n}\r\n\r\nexport interface ObjectStorageOptions {\r\n /** Must already exist. */\r\n bucket: string;\r\n /** Set for anything that is not AWS S3 itself (MinIO, R2, B2, Spaces, ...). */\r\n endpoint?: string;\r\n /** Default: AWS_REGION / AWS_DEFAULT_REGION, else \"us-east-1\". Many S3-compatible services ignore it but the SDK requires one. */\r\n region?: string;\r\n /** Default: the SDK's standard chain (env vars, shared config, IAM role, ...). */\r\n credentials?: {\r\n accessKeyId: string;\r\n secretAccessKey: string;\r\n sessionToken?: string;\r\n };\r\n /** Path-style addressing (`endpoint/bucket/key`). Default: true when `endpoint` is set, else false. */\r\n forcePathStyle?: boolean;\r\n /** Bring your own configured S3Client (advanced). */\r\n client?: unknown;\r\n}\r\n\r\ntype Sdk = typeof import(\"@aws-sdk/client-s3\");\r\ntype S3 = import(\"@aws-sdk/client-s3\").S3Client;\r\n\r\nexport class ObjectStorage implements ObjectStorageLike {\r\n readonly bucket: string;\r\n private clientPromise: Promise<{ sdk: Sdk; client: S3 }> | undefined;\r\n\r\n constructor(private readonly opts: ObjectStorageOptions) {\r\n if (!opts.bucket)\r\n throw new TypeError(\"ObjectStorage requires a bucket name\");\r\n this.bucket = opts.bucket;\r\n }\r\n\r\n private connection(): Promise<{ sdk: Sdk; client: S3 }> {\r\n this.clientPromise ??= (async () => {\r\n let sdk: Sdk;\r\n try {\r\n sdk = await import(\"@aws-sdk/client-s3\");\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"Object-storage tiering requires the optional dependency '@aws-sdk/client-s3'. \" +\r\n \"Install it with: npm install @aws-sdk/client-s3\",\r\n { cause },\r\n );\r\n }\r\n const o = this.opts;\r\n const client =\r\n (o.client as S3 | undefined) ??\r\n new sdk.S3Client({\r\n endpoint: o.endpoint,\r\n region:\r\n o.region ??\r\n process.env.AWS_REGION ??\r\n process.env.AWS_DEFAULT_REGION ??\r\n \"us-east-1\",\r\n credentials: o.credentials,\r\n forcePathStyle: o.forcePathStyle ?? o.endpoint !== undefined,\r\n });\r\n return { sdk, client };\r\n })().catch((exc) => {\r\n this.clientPromise = undefined;\r\n throw exc;\r\n });\r\n return this.clientPromise;\r\n }\r\n\r\n async upload(\r\n key: string,\r\n data: Buffer,\r\n { mimeType }: { mimeType: string },\r\n ): Promise<void> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n await client.send(\r\n new sdk.PutObjectCommand({\r\n Bucket: this.bucket,\r\n Key: key,\r\n Body: data,\r\n ContentType: mimeType,\r\n }),\r\n );\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to upload ${JSON.stringify(key)} to bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async download(key: string): Promise<Buffer> {\r\n return this.get(key, undefined);\r\n }\r\n\r\n async downloadRange(\r\n key: string,\r\n start: number,\r\n endInclusive: number,\r\n ): Promise<Buffer> {\r\n return this.get(key, `bytes=${start}-${endInclusive}`);\r\n }\r\n\r\n private async get(key: string, range: string | undefined): Promise<Buffer> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n const res = await client.send(\r\n new sdk.GetObjectCommand({\r\n Bucket: this.bucket,\r\n Key: key,\r\n Range: range,\r\n }),\r\n );\r\n return Buffer.from(await res.Body!.transformToByteArray());\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to download ${JSON.stringify(key)} from bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n // S3 DeleteObject succeeds for keys that do not exist, so this is idempotent by nature.\r\n await client.send(\r\n new sdk.DeleteObjectCommand({ Bucket: this.bucket, Key: key }),\r\n );\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to delete ${JSON.stringify(key)} from bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n await client.send(\r\n new sdk.HeadObjectCommand({ Bucket: this.bucket, Key: key }),\r\n );\r\n return true;\r\n } catch (exc) {\r\n const e = exc as {\r\n name?: string;\r\n $metadata?: { httpStatusCode?: number };\r\n };\r\n if (\r\n e.name === \"NotFound\" ||\r\n e.name === \"NoSuchKey\" ||\r\n e.$metadata?.httpStatusCode === 404\r\n )\r\n return false;\r\n throw new StorageError(\r\n `Failed to check ${JSON.stringify(key)} in bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n}\r\n\r\nfunction msg(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n","import { ContentValidationError } from \"../errors.js\";\r\nimport type { ContentValidator, ValidatedContent } from \"./types.js\";\r\n\r\nexport const DEFAULT_MAX_PDF_BYTES = 20 * 1024 * 1024;\r\n\r\n/**\r\n * Validates that bytes look like a PDF: `%PDF-` magic bytes, non-empty, under a\r\n * size ceiling. Deliberately minimal.\r\n *\r\n * Security note: this does NOT parse the PDF object structure and does not\r\n * detect embedded JavaScript, forms or launch actions. Treat it as a sanity\r\n * gate, not a security boundary, if you serve user-uploaded PDFs to other users.\r\n */\r\nexport class PDFValidator implements ContentValidator {\r\n constructor(private readonly opts: { maxBytes?: number } = {}) {}\r\n\r\n validate(data: Buffer, { maxBytes }: { maxBytes: number }): ValidatedContent {\r\n const effectiveMax = Math.min(\r\n maxBytes,\r\n this.opts.maxBytes ?? DEFAULT_MAX_PDF_BYTES,\r\n );\r\n if (data.length === 0)\r\n throw new ContentValidationError(\"PDF data is empty\");\r\n if (data.length > effectiveMax) {\r\n throw new ContentValidationError(\r\n `PDF is ${data.length} bytes, exceeds the maximum of ${effectiveMax} bytes`,\r\n );\r\n }\r\n // The spec allows junk before the header; real readers scan the first 1024 bytes.\r\n if (!data.subarray(0, 1024).includes(\"%PDF-\")) {\r\n throw new ContentValidationError(\r\n \"This does not look like a valid PDF (missing '%PDF-' header). Content is \" +\r\n \"checked by inspecting the actual bytes, not the filename or any client-supplied content type.\",\r\n );\r\n }\r\n return {\r\n mimeType: \"application/pdf\",\r\n sizeBytes: data.length,\r\n width: null,\r\n height: null,\r\n };\r\n }\r\n}\r\n"],"mappings":"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","names":["start","SCHEMA","DEDUP_SCHEMA","INSERT","SELECT_FULL","SELECT_METADATA","EXISTS","SELECT_FOR_TIERING","UPDATE_AFTER_TIERING","D_FROM","D_COLS","D_INSERT_REF","D_DELETE_REF","D_DECREMENT","D_DELETE_EMPTY","mapLimit","msg"]}