zerobucket 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts","../src/adapters/postgres.ts","../src/validation.ts","../src/optimize.ts","../src/validators/pdf.ts"],"sourcesContent":["export { ZeroBucket, DEFAULT_MAX_BYTES, readInput } from \"./client.js\";\r\nexport type {\r\n ImageInput,\r\n ZeroBucketOptions,\r\n PutOptions,\r\n PutManyOptions,\r\n GetOptions,\r\n StreamOptions,\r\n WritableLike,\r\n BeforeGetHook,\r\n BeforePutHook,\r\n} from \"./client.js\";\r\n\r\nexport {\r\n PostgresBackend,\r\n DEFAULT_STREAM_CHUNK_SIZE,\r\n} from \"./adapters/postgres.js\";\r\nexport type { PostgresBackendOptions } from \"./adapters/postgres.js\";\r\nexport type {\r\n StorageBackend,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n CallOptions,\r\n} from \"./adapters/base.js\";\r\n\r\nexport type {\r\n Image,\r\n ImageMetadata,\r\n BatchPutResult,\r\n BatchGetResult,\r\n BatchDeleteResult,\r\n OperationEvent,\r\n Queryable,\r\n PreparedRow,\r\n} from \"./types.js\";\r\n\r\nexport {\r\n ZeroBucketError,\r\n ContentValidationError,\r\n ImageValidationError,\r\n ImageTooLargeError,\r\n UnsupportedFormatError,\r\n CorruptedImageError,\r\n ImageNotFoundError,\r\n StorageError,\r\n AccessDeniedError,\r\n} from \"./errors.js\";\r\n\r\nexport {\r\n validateImage,\r\n SUPPORTED_FORMATS,\r\n DEFAULT_MAX_PIXELS,\r\n} from \"./validation.js\";\r\nexport type { ValidatedImage } from \"./validation.js\";\r\nexport { optimizeImage } from \"./optimize.js\";\r\nexport type { OptimizeOptions, OptimizationResult } from \"./optimize.js\";\r\n\r\nexport { PDFValidator } from \"./validators/pdf.js\";\r\nexport type { ContentValidator, ValidatedContent } from \"./validators/types.js\";\r\n","/**\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 * Error hierarchy for ZeroBucket. Mirrors the Python package's exceptions\r\n * one-to-one so behaviour is portable across languages.\r\n *\r\n * Catch broadly with `instanceof ZeroBucketError`, or narrowly with a\r\n * subclass such as `ImageNotFoundError`.\r\n */\r\n\r\nexport class ZeroBucketError extends Error {\r\n constructor(message: string, options?: { cause?: unknown }) {\r\n super(message, options);\r\n this.name = new.target.name;\r\n }\r\n}\r\n\r\n/** Any content (image or custom-validated) failed validation. */\r\nexport class ContentValidationError extends ZeroBucketError {}\r\n\r\n/** An image failed validation (bad format, too large, corrupted, ...). */\r\nexport class ImageValidationError extends ContentValidationError {}\r\n\r\nexport class ImageTooLargeError extends ImageValidationError {\r\n constructor(\r\n public readonly sizeBytes: number,\r\n public readonly maxBytes: number,\r\n ) {\r\n super(\r\n `Image is ${sizeBytes} bytes, which exceeds the maximum of ${maxBytes} bytes`,\r\n );\r\n }\r\n}\r\n\r\nexport class UnsupportedFormatError extends ImageValidationError {\r\n constructor(\r\n public readonly detectedFormat: string | null,\r\n public readonly allowed: readonly string[],\r\n ) {\r\n super(\r\n `Detected format ${JSON.stringify(detectedFormat)} is not supported. ` +\r\n `Allowed formats: ${JSON.stringify([...allowed].sort())}`,\r\n );\r\n }\r\n}\r\n\r\n/** Image bytes could not be decoded despite a recognizable header. */\r\nexport class CorruptedImageError extends ImageValidationError {}\r\n\r\n/** `get()`, `getStream()` or `metadata()` was called with an id that does not exist. */\r\nexport class ImageNotFoundError extends ZeroBucketError {\r\n constructor(public readonly imageId: string) {\r\n super(`No image found with id ${JSON.stringify(imageId)}`);\r\n }\r\n}\r\n\r\n/** Underlying storage / database failure not covered above. */\r\nexport class StorageError extends ZeroBucketError {}\r\n\r\n/**\r\n * A `beforeGet` / `beforePut` hook denied the operation by returning false.\r\n *\r\n * If the hook itself throws, that exception propagates unchanged: it is NOT\r\n * wrapped in AccessDeniedError and NOT treated as an implicit allow. A broken\r\n * authorization check must fail closed, never open.\r\n */\r\nexport class AccessDeniedError extends ZeroBucketError {\r\n constructor(\r\n public readonly operation: string,\r\n public readonly imageId?: string,\r\n ) {\r\n super(\r\n imageId !== undefined\r\n ? `${operation} denied for image ${JSON.stringify(imageId)} by the beforeGet hook`\r\n : `${operation} denied by the beforePut hook`,\r\n );\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 { 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"]}
@@ -0,0 +1,164 @@
1
+ import { S as StorageBackend, O as OperationEvent, P as PreparedRow, C as CallOptions, a as StoredRecord, b as StoredRecordMetadata, c as StreamOptions, d as ContentValidator, V as ValidatedContent } from './client-C_W0t8My.cjs';
2
+ export { B as BatchDeleteResult, e as BatchGetResult, f as BatchPutResult, g as BeforeGetHook, h as BeforePutHook, D as DEFAULT_MAX_BYTES, G as GetOptions, I as Image, i as ImageInput, j as ImageMetadata, k as OptimizationResult, l as OptimizeOptions, m as PutManyOptions, n as PutOptions, Q as Queryable, o as StreamOptions, W as WritableLike, Z as ZeroBucket, p as ZeroBucketOptions, q as optimizeImage, r as readInput } from './client-C_W0t8My.cjs';
3
+ import { Pool } from 'pg';
4
+
5
+ /**
6
+ * PostgreSQL storage adapter (classic mode).
7
+ *
8
+ * Uses the EXACT schema the Python package creates (zerobucket_images, including
9
+ * the object-storage tiering columns and CHECK constraint), so a Node app and a
10
+ * Python app can share one database and read each other's rows.
11
+ *
12
+ * Lessons from the Python build, applied here from day one:
13
+ * - Retry re-runs the WHOLE unit of work on a fresh connection after a full
14
+ * ROLLBACK. It never replays a single statement of a multi-statement
15
+ * transaction (the "half-applied put" hazard).
16
+ * - Retry never applies when the caller passes `connection` (the caller owns
17
+ * the transaction; restarting it is their decision).
18
+ * - Pool acquire has an explicit timeout.
19
+ * - Migration runs under an advisory lock so N concurrent first-callers do not
20
+ * race on CREATE TABLE.
21
+ */
22
+
23
+ /** 1 MiB. Default chunk size for getStream(). */
24
+ declare const DEFAULT_STREAM_CHUNK_SIZE: number;
25
+ interface PostgresBackendOptions {
26
+ /** postgresql:// connection string. Required unless `pool` is given. */
27
+ connectionString?: string;
28
+ /** Bring your own pg.Pool (not closed by `close()`). Pool sizing options are then ignored. */
29
+ pool?: Pool;
30
+ /** Create the schema on first use. Default true. */
31
+ autoMigrate?: boolean;
32
+ /** Retries for transient errors on pooled calls. Default 3; 0 disables. */
33
+ maxRetries?: number;
34
+ /** Base for exponential backoff with jitter (capped at 2s). Default 100. */
35
+ retryBaseDelayMs?: number;
36
+ poolMinSize?: number;
37
+ poolMaxSize?: number;
38
+ /** Max wait for a pooled connection. Default 10000. */
39
+ poolTimeoutMs?: number;
40
+ onOperation?: (event: OperationEvent) => void;
41
+ }
42
+ declare class PostgresBackend implements StorageBackend {
43
+ private readonly opts;
44
+ private pool;
45
+ private readonly ownsPool;
46
+ private readyPromise;
47
+ private closed;
48
+ private readonly maxRetries;
49
+ private readonly retryBaseDelayMs;
50
+ private readonly autoMigrate;
51
+ constructor(opts: PostgresBackendOptions);
52
+ ready(): Promise<void>;
53
+ private init;
54
+ /** Create the schema if missing. Serialized across processes with an advisory lock. */
55
+ migrate(): Promise<void>;
56
+ close(): Promise<void>;
57
+ private emit;
58
+ /**
59
+ * Run `work` and return its result, emitting exactly one OperationEvent.
60
+ *
61
+ * connection given -> run once on it, no BEGIN/COMMIT, no retry.
62
+ * connection absent -> pooled; `tx` wraps the work in BEGIN/COMMIT; transient
63
+ * failures retry the WHOLE closure on a fresh connection.
64
+ */
65
+ private run;
66
+ put(row: PreparedRow, { connection }?: CallOptions): Promise<string>;
67
+ putMany(rows: PreparedRow[], { connection }?: CallOptions): Promise<string[]>;
68
+ get(id: string, { connection }?: CallOptions): Promise<StoredRecord | null>;
69
+ getMany(ids: string[], { connection }?: CallOptions): Promise<StoredRecord[]>;
70
+ getMetadata(id: string, { connection }?: CallOptions): Promise<StoredRecordMetadata | null>;
71
+ getStream(id: string, opts: StreamOptions): Promise<AsyncIterable<Buffer> | null>;
72
+ delete(id: string, { connection }?: CallOptions): Promise<boolean>;
73
+ deleteMany(ids: string[], { connection }?: CallOptions): Promise<string[]>;
74
+ exists(id: string, { connection }?: CallOptions): Promise<boolean>;
75
+ private toRecord;
76
+ private tieredWithoutStorage;
77
+ }
78
+
79
+ /**
80
+ * Error hierarchy for ZeroBucket. Mirrors the Python package's exceptions
81
+ * one-to-one so behaviour is portable across languages.
82
+ *
83
+ * Catch broadly with `instanceof ZeroBucketError`, or narrowly with a
84
+ * subclass such as `ImageNotFoundError`.
85
+ */
86
+ declare class ZeroBucketError extends Error {
87
+ constructor(message: string, options?: {
88
+ cause?: unknown;
89
+ });
90
+ }
91
+ /** Any content (image or custom-validated) failed validation. */
92
+ declare class ContentValidationError extends ZeroBucketError {
93
+ }
94
+ /** An image failed validation (bad format, too large, corrupted, ...). */
95
+ declare class ImageValidationError extends ContentValidationError {
96
+ }
97
+ declare class ImageTooLargeError extends ImageValidationError {
98
+ readonly sizeBytes: number;
99
+ readonly maxBytes: number;
100
+ constructor(sizeBytes: number, maxBytes: number);
101
+ }
102
+ declare class UnsupportedFormatError extends ImageValidationError {
103
+ readonly detectedFormat: string | null;
104
+ readonly allowed: readonly string[];
105
+ constructor(detectedFormat: string | null, allowed: readonly string[]);
106
+ }
107
+ /** Image bytes could not be decoded despite a recognizable header. */
108
+ declare class CorruptedImageError extends ImageValidationError {
109
+ }
110
+ /** `get()`, `getStream()` or `metadata()` was called with an id that does not exist. */
111
+ declare class ImageNotFoundError extends ZeroBucketError {
112
+ readonly imageId: string;
113
+ constructor(imageId: string);
114
+ }
115
+ /** Underlying storage / database failure not covered above. */
116
+ declare class StorageError extends ZeroBucketError {
117
+ }
118
+ /**
119
+ * A `beforeGet` / `beforePut` hook denied the operation by returning false.
120
+ *
121
+ * If the hook itself throws, that exception propagates unchanged: it is NOT
122
+ * wrapped in AccessDeniedError and NOT treated as an implicit allow. A broken
123
+ * authorization check must fail closed, never open.
124
+ */
125
+ declare class AccessDeniedError extends ZeroBucketError {
126
+ readonly operation: string;
127
+ readonly imageId?: string | undefined;
128
+ constructor(operation: string, imageId?: string | undefined);
129
+ }
130
+
131
+ declare const SUPPORTED_FORMATS: readonly string[];
132
+ /** Reject images that would decode to more pixels than this, regardless of compressed size (~89 MP). */
133
+ declare const DEFAULT_MAX_PIXELS = 89000000;
134
+ interface ValidatedImage {
135
+ mimeType: string;
136
+ width: number;
137
+ height: number;
138
+ sizeBytes: number;
139
+ }
140
+ declare function validateImage(data: Buffer, opts: {
141
+ maxBytes: number;
142
+ maxPixels?: number;
143
+ allowedFormats?: readonly string[];
144
+ }): Promise<ValidatedImage>;
145
+
146
+ /**
147
+ * Validates that bytes look like a PDF: `%PDF-` magic bytes, non-empty, under a
148
+ * size ceiling. Deliberately minimal.
149
+ *
150
+ * Security note: this does NOT parse the PDF object structure and does not
151
+ * detect embedded JavaScript, forms or launch actions. Treat it as a sanity
152
+ * gate, not a security boundary, if you serve user-uploaded PDFs to other users.
153
+ */
154
+ declare class PDFValidator implements ContentValidator {
155
+ private readonly opts;
156
+ constructor(opts?: {
157
+ maxBytes?: number;
158
+ });
159
+ validate(data: Buffer, { maxBytes }: {
160
+ maxBytes: number;
161
+ }): ValidatedContent;
162
+ }
163
+
164
+ export { AccessDeniedError, CallOptions, ContentValidationError, ContentValidator, CorruptedImageError, DEFAULT_MAX_PIXELS, DEFAULT_STREAM_CHUNK_SIZE, ImageNotFoundError, ImageTooLargeError, ImageValidationError, OperationEvent, PDFValidator, PostgresBackend, type PostgresBackendOptions, PreparedRow, SUPPORTED_FORMATS, StorageBackend, StorageError, StoredRecord, StoredRecordMetadata, UnsupportedFormatError, ValidatedContent, type ValidatedImage, ZeroBucketError, validateImage };
@@ -0,0 +1,164 @@
1
+ import { S as StorageBackend, O as OperationEvent, P as PreparedRow, C as CallOptions, a as StoredRecord, b as StoredRecordMetadata, c as StreamOptions, d as ContentValidator, V as ValidatedContent } from './client-C_W0t8My.js';
2
+ export { B as BatchDeleteResult, e as BatchGetResult, f as BatchPutResult, g as BeforeGetHook, h as BeforePutHook, D as DEFAULT_MAX_BYTES, G as GetOptions, I as Image, i as ImageInput, j as ImageMetadata, k as OptimizationResult, l as OptimizeOptions, m as PutManyOptions, n as PutOptions, Q as Queryable, o as StreamOptions, W as WritableLike, Z as ZeroBucket, p as ZeroBucketOptions, q as optimizeImage, r as readInput } from './client-C_W0t8My.js';
3
+ import { Pool } from 'pg';
4
+
5
+ /**
6
+ * PostgreSQL storage adapter (classic mode).
7
+ *
8
+ * Uses the EXACT schema the Python package creates (zerobucket_images, including
9
+ * the object-storage tiering columns and CHECK constraint), so a Node app and a
10
+ * Python app can share one database and read each other's rows.
11
+ *
12
+ * Lessons from the Python build, applied here from day one:
13
+ * - Retry re-runs the WHOLE unit of work on a fresh connection after a full
14
+ * ROLLBACK. It never replays a single statement of a multi-statement
15
+ * transaction (the "half-applied put" hazard).
16
+ * - Retry never applies when the caller passes `connection` (the caller owns
17
+ * the transaction; restarting it is their decision).
18
+ * - Pool acquire has an explicit timeout.
19
+ * - Migration runs under an advisory lock so N concurrent first-callers do not
20
+ * race on CREATE TABLE.
21
+ */
22
+
23
+ /** 1 MiB. Default chunk size for getStream(). */
24
+ declare const DEFAULT_STREAM_CHUNK_SIZE: number;
25
+ interface PostgresBackendOptions {
26
+ /** postgresql:// connection string. Required unless `pool` is given. */
27
+ connectionString?: string;
28
+ /** Bring your own pg.Pool (not closed by `close()`). Pool sizing options are then ignored. */
29
+ pool?: Pool;
30
+ /** Create the schema on first use. Default true. */
31
+ autoMigrate?: boolean;
32
+ /** Retries for transient errors on pooled calls. Default 3; 0 disables. */
33
+ maxRetries?: number;
34
+ /** Base for exponential backoff with jitter (capped at 2s). Default 100. */
35
+ retryBaseDelayMs?: number;
36
+ poolMinSize?: number;
37
+ poolMaxSize?: number;
38
+ /** Max wait for a pooled connection. Default 10000. */
39
+ poolTimeoutMs?: number;
40
+ onOperation?: (event: OperationEvent) => void;
41
+ }
42
+ declare class PostgresBackend implements StorageBackend {
43
+ private readonly opts;
44
+ private pool;
45
+ private readonly ownsPool;
46
+ private readyPromise;
47
+ private closed;
48
+ private readonly maxRetries;
49
+ private readonly retryBaseDelayMs;
50
+ private readonly autoMigrate;
51
+ constructor(opts: PostgresBackendOptions);
52
+ ready(): Promise<void>;
53
+ private init;
54
+ /** Create the schema if missing. Serialized across processes with an advisory lock. */
55
+ migrate(): Promise<void>;
56
+ close(): Promise<void>;
57
+ private emit;
58
+ /**
59
+ * Run `work` and return its result, emitting exactly one OperationEvent.
60
+ *
61
+ * connection given -> run once on it, no BEGIN/COMMIT, no retry.
62
+ * connection absent -> pooled; `tx` wraps the work in BEGIN/COMMIT; transient
63
+ * failures retry the WHOLE closure on a fresh connection.
64
+ */
65
+ private run;
66
+ put(row: PreparedRow, { connection }?: CallOptions): Promise<string>;
67
+ putMany(rows: PreparedRow[], { connection }?: CallOptions): Promise<string[]>;
68
+ get(id: string, { connection }?: CallOptions): Promise<StoredRecord | null>;
69
+ getMany(ids: string[], { connection }?: CallOptions): Promise<StoredRecord[]>;
70
+ getMetadata(id: string, { connection }?: CallOptions): Promise<StoredRecordMetadata | null>;
71
+ getStream(id: string, opts: StreamOptions): Promise<AsyncIterable<Buffer> | null>;
72
+ delete(id: string, { connection }?: CallOptions): Promise<boolean>;
73
+ deleteMany(ids: string[], { connection }?: CallOptions): Promise<string[]>;
74
+ exists(id: string, { connection }?: CallOptions): Promise<boolean>;
75
+ private toRecord;
76
+ private tieredWithoutStorage;
77
+ }
78
+
79
+ /**
80
+ * Error hierarchy for ZeroBucket. Mirrors the Python package's exceptions
81
+ * one-to-one so behaviour is portable across languages.
82
+ *
83
+ * Catch broadly with `instanceof ZeroBucketError`, or narrowly with a
84
+ * subclass such as `ImageNotFoundError`.
85
+ */
86
+ declare class ZeroBucketError extends Error {
87
+ constructor(message: string, options?: {
88
+ cause?: unknown;
89
+ });
90
+ }
91
+ /** Any content (image or custom-validated) failed validation. */
92
+ declare class ContentValidationError extends ZeroBucketError {
93
+ }
94
+ /** An image failed validation (bad format, too large, corrupted, ...). */
95
+ declare class ImageValidationError extends ContentValidationError {
96
+ }
97
+ declare class ImageTooLargeError extends ImageValidationError {
98
+ readonly sizeBytes: number;
99
+ readonly maxBytes: number;
100
+ constructor(sizeBytes: number, maxBytes: number);
101
+ }
102
+ declare class UnsupportedFormatError extends ImageValidationError {
103
+ readonly detectedFormat: string | null;
104
+ readonly allowed: readonly string[];
105
+ constructor(detectedFormat: string | null, allowed: readonly string[]);
106
+ }
107
+ /** Image bytes could not be decoded despite a recognizable header. */
108
+ declare class CorruptedImageError extends ImageValidationError {
109
+ }
110
+ /** `get()`, `getStream()` or `metadata()` was called with an id that does not exist. */
111
+ declare class ImageNotFoundError extends ZeroBucketError {
112
+ readonly imageId: string;
113
+ constructor(imageId: string);
114
+ }
115
+ /** Underlying storage / database failure not covered above. */
116
+ declare class StorageError extends ZeroBucketError {
117
+ }
118
+ /**
119
+ * A `beforeGet` / `beforePut` hook denied the operation by returning false.
120
+ *
121
+ * If the hook itself throws, that exception propagates unchanged: it is NOT
122
+ * wrapped in AccessDeniedError and NOT treated as an implicit allow. A broken
123
+ * authorization check must fail closed, never open.
124
+ */
125
+ declare class AccessDeniedError extends ZeroBucketError {
126
+ readonly operation: string;
127
+ readonly imageId?: string | undefined;
128
+ constructor(operation: string, imageId?: string | undefined);
129
+ }
130
+
131
+ declare const SUPPORTED_FORMATS: readonly string[];
132
+ /** Reject images that would decode to more pixels than this, regardless of compressed size (~89 MP). */
133
+ declare const DEFAULT_MAX_PIXELS = 89000000;
134
+ interface ValidatedImage {
135
+ mimeType: string;
136
+ width: number;
137
+ height: number;
138
+ sizeBytes: number;
139
+ }
140
+ declare function validateImage(data: Buffer, opts: {
141
+ maxBytes: number;
142
+ maxPixels?: number;
143
+ allowedFormats?: readonly string[];
144
+ }): Promise<ValidatedImage>;
145
+
146
+ /**
147
+ * Validates that bytes look like a PDF: `%PDF-` magic bytes, non-empty, under a
148
+ * size ceiling. Deliberately minimal.
149
+ *
150
+ * Security note: this does NOT parse the PDF object structure and does not
151
+ * detect embedded JavaScript, forms or launch actions. Treat it as a sanity
152
+ * gate, not a security boundary, if you serve user-uploaded PDFs to other users.
153
+ */
154
+ declare class PDFValidator implements ContentValidator {
155
+ private readonly opts;
156
+ constructor(opts?: {
157
+ maxBytes?: number;
158
+ });
159
+ validate(data: Buffer, { maxBytes }: {
160
+ maxBytes: number;
161
+ }): ValidatedContent;
162
+ }
163
+
164
+ export { AccessDeniedError, CallOptions, ContentValidationError, ContentValidator, CorruptedImageError, DEFAULT_MAX_PIXELS, DEFAULT_STREAM_CHUNK_SIZE, ImageNotFoundError, ImageTooLargeError, ImageValidationError, OperationEvent, PDFValidator, PostgresBackend, type PostgresBackendOptions, PreparedRow, SUPPORTED_FORMATS, StorageBackend, StorageError, StoredRecord, StoredRecordMetadata, UnsupportedFormatError, ValidatedContent, type ValidatedImage, ZeroBucketError, validateImage };