@powerhousedao/reactor-attachments 6.2.3-dev.54 → 6.2.3-dev.56
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/client.d.ts +6 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -18
- package/dist/client.js.map +1 -1
- package/dist/hash-BobbR4r4.js +63 -0
- package/dist/hash-BobbR4r4.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/{interfaces-jHW9JXv1.d.ts → interfaces-C4GRlKeB.d.ts} +5 -2
- package/dist/{interfaces-jHW9JXv1.d.ts.map → interfaces-C4GRlKeB.d.ts.map} +1 -1
- package/dist/{null-attachment-transport-dg7vi6F4.d.ts → null-attachment-transport-Bs4JpmFx.d.ts} +3 -3
- package/dist/{null-attachment-transport-dg7vi6F4.d.ts.map → null-attachment-transport-Bs4JpmFx.d.ts.map} +1 -1
- package/dist/{null-attachment-transport-BGG9cRC-.js → null-attachment-transport-Cm_kb5ww.js} +17 -4
- package/dist/null-attachment-transport-Cm_kb5ww.js.map +1 -0
- package/dist/replication.d.ts +1 -1
- package/dist/replication.d.ts.map +1 -1
- package/dist/replication.js +1 -1
- package/package.json +4 -3
- package/dist/hash-CKHeI4AH.js +0 -26
- package/dist/hash-CKHeI4AH.js.map +0 -1
- package/dist/null-attachment-transport-BGG9cRC-.js.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["rowToHeader","up","down","up","down","up","down","up","down","up","down","up","down","migrations","migration001","migration002","migration003","migration004","migration005","migration006","ProgrammaticMigrationProvider","migration001","Buffer"],"sources":["../src/reserve-metadata.ts","../src/storage/fs/attachment-fs.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/001_create_attachment_table.ts","../src/storage/migrations/002_create_reservation_table.ts","../src/storage/migrations/003_add_reservation_expires_at.ts","../src/storage/migrations/004_add_reservation_soft_delete.ts","../src/storage/migrations/005_add_reservation_active_index.ts","../src/storage/migrations/006_add_reservation_client_hash.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/direct/filesystem-attachment-backend.ts","../src/storage/s3/upload-factory.ts","../src/attachment-builder.ts","../src/read-models/attachment-reference/kysely-attachment-reference-store.ts","../src/read-models/attachment-reference/storage/migrations/001_create_attachment_reference_table.ts","../src/read-models/attachment-reference/storage/migrations/migrator.ts","../src/read-models/attachment-reference/index-builder.ts","../src/read-models/attachment-reference/attachment-reference-read-model.ts","../src/storage/s3/config.ts","../src/storage/s3/keying.ts","../src/storage/s3/primitives.ts","../src/storage/s3/backend.ts"],"sourcesContent":["import { InvalidAttachmentMetadata } from \"./errors.js\";\n\n// eslint-disable-next-line no-control-regex\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f]/;\n// Characters a header value cannot carry: CTLs other than HTAB, DEL, and\n// anything above 0xFF.\n// eslint-disable-next-line no-control-regex\nconst NON_HEADER_CHARS = /[^\\t\\x20-\\x7e\\x80-\\xff]/;\n// RFC 6838 tokens, with optional `; param=value` (token or quoted-string).\nconst MIME_TYPE_PATTERN =\n /^[!#$%&'*+\\-.^_`|~\\w]+\\/[!#$%&'*+\\-.^_`|~\\w]+(?:[ \\t]*;[ \\t]*[!#$%&'*+\\-.^_`|~\\w]+=(?:[!#$%&'*+\\-.^_`|~\\w]+|\"(?:[^\"\\\\\\r\\n]|\\\\[^\\r\\n])*\"))*$/;\nconst MAX_FILENAME_LEN = 255;\nconst MAX_MIMETYPE_LEN = 255;\n\n/** The descriptive fields of a reservation, as a caller supplies them. */\nexport type ReserveMetadata = {\n mimeType: string;\n fileName: string;\n extension?: string | null;\n};\n\n/**\n * Validates reserve metadata with the rules the switchboard reserve route\n * applies.\n *\n * @throws InvalidAttachmentMetadata naming the first field that fails.\n */\nexport function validateReserveMetadata(meta: ReserveMetadata): void {\n const { mimeType, fileName, extension } = meta as Record<string, unknown>;\n if (\n typeof mimeType !== \"string\" ||\n mimeType.length === 0 ||\n mimeType.length > MAX_MIMETYPE_LEN ||\n NON_HEADER_CHARS.test(mimeType) ||\n !MIME_TYPE_PATTERN.test(mimeType)\n ) {\n throw new InvalidAttachmentMetadata(\"mimeType\");\n }\n if (\n typeof fileName !== \"string\" ||\n fileName.length === 0 ||\n fileName.length > MAX_FILENAME_LEN ||\n CONTROL_CHARS.test(fileName)\n ) {\n throw new InvalidAttachmentMetadata(\"fileName\");\n }\n if (typeof extension === \"string\") {\n if (extension.length === 0 || /[\\\\/]/.test(extension)) {\n throw new InvalidAttachmentMetadata(\"extension\");\n }\n } else if (extension !== undefined && extension !== null) {\n throw new InvalidAttachmentMetadata(\"extension\");\n }\n}\n","import { mkdir, rm, rename, access } from \"node:fs/promises\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { join, dirname } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { finished } from \"node:stream/promises\";\nimport { SizeMismatch, UploadTooLarge } from \"../../errors.js\";\n\n/**\n * Compute the absolute storage path for an attachment hash.\n * Uses a 2-level directory fan-out to avoid millions of files\n * in a single directory: ab/cd/abcdef123456...\n */\nexport function storagePath(basePath: string, hash: string): string {\n return join(basePath, storageRelativePath(hash));\n}\n\n/**\n * Compute the relative storage path for an attachment hash.\n * This is what gets stored in the database's storage_path column.\n */\nexport function storageRelativePath(hash: string): string {\n return join(hash.slice(0, 2), hash.slice(2, 4), hash);\n}\n\n/**\n * Write a ReadableStream to disk. Creates parent directories as needed.\n *\n * Bytes are streamed to a uniquely-named temp file in the destination\n * directory and only atomically renamed onto the final path once the whole\n * stream has been written and flushed. The final path therefore never holds a\n * partial or torn file: a concurrent writer of the same content-addressed\n * path (e.g. two clients re-fetching the same evicted attachment) streams to\n * its own temp file, and the rename publishes the new content all at once. On\n * any failure the temp file is removed and the previous file (if any) is left\n * untouched.\n *\n * Returns the number of bytes written.\n */\n// createWriteStream opens the file lazily, so destroy() can return before the\n// open has even happened and the open then creates the file after the unlink\n// below has run. Wait for the stream to close first, or the failed write\n// leaves its temp file behind.\nasync function discardTempFile(\n writer: ReturnType<typeof createWriteStream>,\n tempPath: string,\n): Promise<void> {\n writer.destroy();\n try {\n await finished(writer, { error: false });\n } catch {\n // The stream is already closed, which is all this wait is for.\n }\n await rm(tempPath, { force: true });\n}\n\nexport async function writeAttachmentBytes(\n path: string,\n data: ReadableStream<Uint8Array>,\n): Promise<number> {\n await mkdir(dirname(path), { recursive: true });\n\n const tempPath = join(dirname(path), `${randomUUID()}.tmp`);\n const writer = createWriteStream(tempPath);\n const reader = data.getReader();\n let bytesWritten = 0;\n let caughtError: Error | undefined;\n // A destination error can land before the drain-wait below has an 'error'\n // listener attached -- failing to open the temp file, or an async write\n // error between chunks -- so hold one listener for the whole write. First\n // error wins: an already-recorded source failure is the one to report.\n const recordError = (err: unknown) => {\n caughtError ??= err instanceof Error ? err : new Error(String(err));\n };\n writer.on(\"error\", recordError);\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n // The destination is destroyed once it errors, so writing again would\n // return false and wait for a 'drain' that can never come.\n if (caughtError) break;\n bytesWritten += value.byteLength;\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n writer.off(\"error\", onError);\n resolve();\n };\n const onError = (err: Error) => {\n writer.off(\"drain\", onDrain);\n reject(err);\n };\n writer.once(\"drain\", onDrain);\n writer.once(\"error\", onError);\n });\n }\n }\n } catch (err) {\n recordError(err);\n } finally {\n reader.releaseLock();\n }\n\n if (caughtError) {\n await discardTempFile(writer, tempPath);\n throw caughtError;\n }\n\n try {\n await new Promise<void>((resolve, reject) => {\n writer.end((err?: Error | null) => {\n if (err) reject(err);\n else resolve();\n });\n writer.once(\"error\", reject);\n });\n await rename(tempPath, path);\n } catch (err) {\n await discardTempFile(writer, tempPath);\n throw err instanceof Error ? err : new Error(String(err));\n }\n\n return bytesWritten;\n}\n\n/**\n * Open a ReadableStream from a file on disk.\n */\nexport function readAttachmentStream(path: string): ReadableStream<Uint8Array> {\n const nodeStream = createReadStream(path);\n return Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;\n}\n\n/**\n * Delete a file from disk. No-op if the file does not exist.\n */\nexport async function deleteAttachmentBytes(path: string): Promise<void> {\n await rm(path, { force: true });\n}\n\n/**\n * Check whether a file exists on disk.\n */\nexport async function attachmentBytesExist(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create a ReadableStream from an in-memory buffer.\n */\nexport function streamFromBuffer(data: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(data);\n controller.close();\n },\n });\n}\n\n/**\n * Stream bytes to a temp file under `${basePath}/.tmp/` while computing the\n * SHA-256 hash, returning the temp path, hex hash, and total bytes written.\n *\n * Bytes are never buffered in memory beyond the current chunk. The caller is\n * responsible for renaming the temp file to its final hash-derived location\n * (or removing it if the content is a duplicate).\n *\n * If `maxBytes` is set and the input exceeds it, the temp file is removed and\n * `UploadTooLarge` is thrown.\n *\n * If `declaredSizeBytes` is set, the byte count is enforced as a contract:\n * mid-stream, the moment the count exceeds the declaration the reader is\n * released and `SizeMismatch` is thrown without consuming the rest of the\n * stream. At stream end, if the count does not equal the declaration,\n * `SizeMismatch` is thrown. Both the `maxBytes` and `declaredSizeBytes`\n * checks apply; `maxBytes` is evaluated first on each chunk.\n *\n * If `signal` aborts, the temp file is removed and the signal's reason is\n * thrown. Nothing partial is ever returned, so a cancelled transfer cannot be\n * committed.\n */\nexport async function streamHashAndWrite(\n basePath: string,\n data: ReadableStream<Uint8Array>,\n options: {\n maxBytes?: number;\n declaredSizeBytes?: number;\n signal?: AbortSignal;\n } = {},\n): Promise<{ tempPath: string; hash: string; sizeBytes: number }> {\n const { maxBytes, declaredSizeBytes, signal } = options;\n // Before the temp file exists: an already-aborted transfer leaves no trace.\n signal?.throwIfAborted();\n const tmpDir = join(basePath, \".tmp\");\n await mkdir(tmpDir, { recursive: true });\n const tempPath = join(tmpDir, randomUUID());\n\n const hasher = createHash(\"sha256\");\n const writer = createWriteStream(tempPath);\n const reader = data.getReader();\n // Cancelling the source is what makes an abort observable while a read is\n // stalled -- it resolves the pending read as `done`. The post-loop\n // `throwIfAborted` is what stops that resolution being mistaken for the end\n // of the stream, which would commit a truncated attachment.\n const onAbort = () => {\n reader.cancel(signal?.reason).catch(() => {});\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n let sizeBytes = 0;\n let caughtError: Error | undefined;\n\n try {\n for (;;) {\n signal?.throwIfAborted();\n const { done, value } = await reader.read();\n if (done) break;\n sizeBytes += value.byteLength;\n if (maxBytes !== undefined && sizeBytes > maxBytes) {\n throw new UploadTooLarge(maxBytes);\n }\n if (declaredSizeBytes !== undefined && sizeBytes > declaredSizeBytes) {\n throw new SizeMismatch(declaredSizeBytes, sizeBytes);\n }\n hasher.update(value);\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n writer.off(\"error\", onError);\n resolve();\n };\n const onError = (err: Error) => {\n writer.off(\"drain\", onDrain);\n reject(err);\n };\n writer.once(\"drain\", onDrain);\n writer.once(\"error\", onError);\n });\n }\n }\n signal?.throwIfAborted();\n } catch (err) {\n caughtError = err instanceof Error ? err : new Error(String(err));\n } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n reader.releaseLock();\n }\n\n let endError: Error | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n writer.end((err?: Error | null) => {\n if (err) reject(err);\n else resolve();\n });\n writer.once(\"error\", reject);\n });\n } catch (err) {\n endError = err instanceof Error ? err : new Error(String(err));\n }\n\n if (caughtError) {\n await rm(tempPath, { force: true });\n throw caughtError;\n }\n if (endError) {\n await rm(tempPath, { force: true });\n throw endError;\n }\n\n if (declaredSizeBytes !== undefined && sizeBytes !== declaredSizeBytes) {\n await rm(tempPath, { force: true });\n throw new SizeMismatch(declaredSizeBytes, sizeBytes);\n }\n\n return {\n tempPath,\n hash: hasher.digest(\"hex\"),\n sizeBytes,\n };\n}\n","import { join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\nimport type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentStore,\n IAttachmentTransport,\n} from \"../../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentStatus,\n} from \"../../types.js\";\nimport { AttachmentNotFound, AttachmentPending } from \"../../errors.js\";\nimport type { AttachmentDatabase, AttachmentRow } from \"./types.js\";\nimport {\n storageRelativePath,\n writeAttachmentBytes,\n readAttachmentStream,\n deleteAttachmentBytes,\n} from \"../fs/attachment-fs.js\";\n\nfunction rowToHeader(row: AttachmentRow): AttachmentHeader {\n return {\n hash: row.hash,\n mimeType: row.mime_type,\n fileName: row.file_name,\n sizeBytes: Number(row.size_bytes),\n extension: row.extension,\n status: row.status as AttachmentStatus,\n source: row.source as \"local\" | \"sync\",\n createdAtUtc: row.created_at_utc,\n lastAccessedAtUtc: row.last_accessed_at_utc,\n expiresAtUtc: null,\n };\n}\n\nfunction wrapStreamWithCleanup(\n source: ReadableStream<Uint8Array>,\n cleanup: () => void,\n): ReadableStream<Uint8Array> {\n let cleaned = false;\n const doCleanup = () => {\n if (!cleaned) {\n cleaned = true;\n cleanup();\n }\n };\n\n const reader = source.getReader();\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) {\n doCleanup();\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (err) {\n doCleanup();\n controller.error(err);\n }\n },\n cancel() {\n doCleanup();\n reader.cancel().catch(() => {});\n },\n });\n}\n\nexport class KyselyAttachmentStore implements IAttachmentStore {\n private readonly activeReaders = new Map<string, number>();\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly transport: IAttachmentTransport,\n private readonly basePath: string,\n ) {}\n\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (row) {\n return rowToHeader(row);\n }\n\n const now = new Date().toISOString();\n const pending = await this.findPendingReservation(hash, now);\n\n if (pending) {\n return {\n hash,\n mimeType: pending.mime_type,\n fileName: pending.file_name,\n sizeBytes: Number(pending.size_bytes),\n extension: pending.extension,\n status: \"pending\",\n source: \"local\",\n createdAtUtc: pending.created_at_utc,\n lastAccessedAtUtc: pending.created_at_utc,\n expiresAtUtc: pending.expires_at_utc,\n };\n }\n\n throw new AttachmentNotFound(hash);\n }\n\n async has(hash: AttachmentHash): Promise<boolean> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .select(\"status\")\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n return row?.status === \"available\";\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n documentId?: string,\n ): Promise<AttachmentResponse> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (row) {\n if (row.status === \"evicted\") {\n return this.fetchRemote(hash, signal, documentId);\n }\n\n const now = new Date().toISOString();\n await this.db\n .updateTable(\"attachment\")\n .set({ last_accessed_at_utc: now })\n .where(\"hash\", \"=\", hash)\n .execute();\n\n const header = rowToHeader(row);\n header.lastAccessedAtUtc = now;\n\n this.acquireReader(hash);\n\n const fullPath = join(this.basePath, row.storage_path);\n const rawStream = readAttachmentStream(fullPath);\n const body = wrapStreamWithCleanup(rawStream, () =>\n this.releaseReader(hash),\n );\n\n return { header, body };\n }\n\n const now = new Date().toISOString();\n const pending = await this.findPendingReservation(hash, now);\n\n if (pending) {\n throw new AttachmentPending(hash, pending.expires_at_utc, {\n mimeType: pending.mime_type,\n fileName: pending.file_name,\n sizeBytes: pending.size_bytes,\n });\n }\n\n return this.fetchRemote(hash, signal, documentId);\n }\n\n async put(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const existing = await this.db\n .selectFrom(\"attachment\")\n .select([\"hash\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (existing?.status === \"available\") {\n await data.cancel();\n return;\n }\n\n const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await writeAttachmentBytes(fullPath, data);\n\n const now = new Date().toISOString();\n\n if (!existing) {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: metadata.mimeType,\n file_name: metadata.fileName,\n size_bytes: metadata.sizeBytes,\n extension: metadata.extension ?? null,\n status: \"available\",\n storage_path: relPath,\n source: \"sync\",\n created_at_utc: metadata.createdAtUtc,\n last_accessed_at_utc: now,\n })\n .onConflict((oc) => oc.column(\"hash\").doNothing())\n .execute();\n } else {\n await this.db\n .updateTable(\"attachment\")\n .set({\n status: \"available\",\n storage_path: relPath,\n last_accessed_at_utc: now,\n })\n .where(\"hash\", \"=\", hash)\n .where(\"status\", \"=\", \"evicted\")\n .execute();\n }\n }\n\n async evict(hash: AttachmentHash): Promise<void> {\n if (this.hasActiveReaders(hash)) {\n return;\n }\n\n const row = await this.db\n .selectFrom(\"attachment\")\n .select([\"storage_path\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (!row || row.status === \"evicted\") {\n return;\n }\n\n const fullPath = join(this.basePath, row.storage_path);\n await deleteAttachmentBytes(fullPath);\n\n await this.db\n .updateTable(\"attachment\")\n .set({ status: \"evicted\" })\n .where(\"hash\", \"=\", hash)\n .execute();\n }\n\n async storageUsed(): Promise<number> {\n const result = await this.db\n .selectFrom(\"attachment\")\n .select(sql<string>`COALESCE(SUM(size_bytes), 0)`.as(\"total\"))\n .where(\"status\", \"=\", \"available\")\n .executeTakeFirst();\n\n return Number(result?.total ?? 0);\n }\n\n // Private: remote re-fetch, pending reservation lookup and active reader tracking\n\n private async fetchRemote(\n hash: AttachmentHash,\n signal: AbortSignal | undefined,\n documentId: string | undefined,\n ): Promise<AttachmentResponse> {\n if (documentId === undefined) {\n throw new AttachmentNotFound(hash);\n }\n const remote = await this.transport.fetch(hash, documentId, signal);\n if (remote.kind === \"data\") {\n await this.put(hash, remote.response.metadata, remote.response.body);\n return this.get(hash, signal);\n }\n if (remote.kind === \"pending\") {\n throw new AttachmentPending(hash, remote.expiresAtUtc);\n }\n throw new AttachmentNotFound(hash);\n }\n\n private async findPendingReservation(\n hash: AttachmentHash,\n now: string,\n ): Promise<{\n mime_type: string;\n file_name: string;\n extension: string | null;\n size_bytes: number;\n created_at_utc: string;\n expires_at_utc: string;\n } | null> {\n const row = await this.db\n .selectFrom(\"attachment_reservation as r\")\n .leftJoin(\"attachment as a\", \"a.hash\", \"r.client_hash\")\n .select([\n \"r.mime_type\",\n \"r.file_name\",\n \"r.extension\",\n \"r.size_bytes\",\n \"r.created_at_utc\",\n \"r.expires_at_utc\",\n ])\n .where(\"r.client_hash\", \"=\", hash)\n .where(\"r.deleted_at_utc\", \"is\", null)\n .where(\"r.expires_at_utc\", \">\", now)\n .where(\"r.size_bytes\", \"is not\", null)\n .where(\"a.hash\", \"is\", null)\n .orderBy(\"r.expires_at_utc\", \"desc\")\n .executeTakeFirst();\n\n if (!row) return null;\n return {\n mime_type: row.mime_type,\n file_name: row.file_name,\n extension: row.extension,\n size_bytes: Number(row.size_bytes),\n created_at_utc: row.created_at_utc,\n expires_at_utc: row.expires_at_utc,\n };\n }\n\n private acquireReader(hash: string): void {\n this.activeReaders.set(hash, (this.activeReaders.get(hash) ?? 0) + 1);\n }\n\n private releaseReader(hash: string): void {\n const count = (this.activeReaders.get(hash) ?? 1) - 1;\n if (count <= 0) {\n this.activeReaders.delete(hash);\n } else {\n this.activeReaders.set(hash, count);\n }\n }\n\n private hasActiveReaders(hash: string): boolean {\n return (this.activeReaders.get(hash) ?? 0) > 0;\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { Kysely } from \"kysely\";\nimport type { IReservationStore } from \"../../interfaces.js\";\nimport type { Reservation, ReserveAttachmentOptions } from \"../../types.js\";\nimport { ReservationNotFound } from \"../../errors.js\";\nimport type { AttachmentDatabase, ReservationRow } from \"./types.js\";\n\nexport const DEFAULT_RESERVATION_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction rowToReservation(row: ReservationRow): Reservation {\n return {\n reservationId: row.reservation_id,\n mimeType: row.mime_type,\n fileName: row.file_name,\n extension: row.extension,\n createdAtUtc: row.created_at_utc,\n expiresAtUtc: row.expires_at_utc,\n clientHash: row.client_hash,\n sizeBytes: row.size_bytes !== null ? Number(row.size_bytes) : null,\n };\n}\n\nexport class KyselyReservationStore implements IReservationStore {\n private readonly ttlMs: number;\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n ttlMs: number = DEFAULT_RESERVATION_TTL_MS,\n ) {\n this.ttlMs = ttlMs;\n }\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const reservationId = randomUUID();\n const nowMs = Date.now();\n const now = new Date(nowMs).toISOString();\n const expiresAt = new Date(nowMs + this.ttlMs).toISOString();\n\n const row = await this.db\n .insertInto(\"attachment_reservation\")\n .values({\n reservation_id: reservationId,\n mime_type: options.mimeType,\n file_name: options.fileName,\n extension: options.extension ?? null,\n created_at_utc: now,\n expires_at_utc: expiresAt,\n client_hash: options.clientHash ?? null,\n size_bytes: options.sizeBytes ?? null,\n })\n .returningAll()\n .executeTakeFirstOrThrow();\n\n return rowToReservation(row);\n }\n\n async get(reservationId: string): Promise<Reservation> {\n const row = await this.db\n .selectFrom(\"attachment_reservation\")\n .selectAll()\n .where(\"reservation_id\", \"=\", reservationId)\n .where(\"deleted_at_utc\", \"is\", null)\n .executeTakeFirst();\n\n if (!row) {\n throw new ReservationNotFound(reservationId);\n }\n\n return rowToReservation(row);\n }\n\n async delete(reservationId: string): Promise<void> {\n await this.db\n .updateTable(\"attachment_reservation\")\n .set({ deleted_at_utc: new Date().toISOString() })\n .where(\"reservation_id\", \"=\", reservationId)\n .where(\"deleted_at_utc\", \"is\", null)\n .execute();\n }\n\n async deleteExpired(now: Date = new Date()): Promise<number> {\n const nowIso = now.toISOString();\n const result = await this.db\n .updateTable(\"attachment_reservation\")\n .set({ deleted_at_utc: nowIso })\n .where(\"expires_at_utc\", \"<=\", nowIso)\n .where(\"deleted_at_utc\", \"is\", null)\n .executeTakeFirst();\n\n return Number(result.numUpdatedRows);\n }\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment\")\n .addColumn(\"hash\", \"text\", (col) => col.primaryKey())\n .addColumn(\"mime_type\", \"text\", (col) => col.notNull())\n .addColumn(\"file_name\", \"text\", (col) => col.notNull())\n .addColumn(\"size_bytes\", \"bigint\", (col) => col.notNull())\n .addColumn(\"extension\", \"text\")\n .addColumn(\"status\", \"text\", (col) => col.notNull().defaultTo(\"available\"))\n .addColumn(\"storage_path\", \"text\", (col) => col.notNull())\n .addColumn(\"source\", \"text\", (col) => col.notNull().defaultTo(\"local\"))\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .addColumn(\"last_accessed_at_utc\", \"text\", (col) => col.notNull())\n .execute();\n\n await db.schema\n .createIndex(\"idx_attachment_status\")\n .on(\"attachment\")\n .column(\"status\")\n .execute();\n\n // Compound index serves the LRU eviction query:\n // SELECT ... WHERE status = 'available' ORDER BY last_accessed_at_utc ASC\n // A partial index would be ideal but raw SQL doesn't respect withSchema().\n await db.schema\n .createIndex(\"idx_attachment_lru\")\n .on(\"attachment\")\n .columns([\"status\", \"last_accessed_at_utc\"])\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment\").ifExists().execute();\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment_reservation\")\n .addColumn(\"reservation_id\", \"text\", (col) => col.primaryKey())\n .addColumn(\"mime_type\", \"text\", (col) => col.notNull())\n .addColumn(\"file_name\", \"text\", (col) => col.notNull())\n .addColumn(\"extension\", \"text\")\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment_reservation\").ifExists().execute();\n}\n","import { sql, type Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"expires_at_utc\", \"text\")\n .execute();\n\n await db\n .updateTable(\"attachment_reservation\")\n .set({ expires_at_utc: sql`created_at_utc` })\n .where(\"expires_at_utc\", \"is\", null)\n .execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .alterColumn(\"expires_at_utc\", (col) => col.setNotNull())\n .execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_expires_at\").ifExists().execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"expires_at_utc\")\n .execute();\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"deleted_at_utc\", \"text\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"deleted_at_utc\")\n .execute();\n}\n","import { sql, type Kysely, type SqlBool } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_expires_at\").ifExists().execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at_active\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .where(sql<SqlBool>`deleted_at_utc IS NULL`)\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema\n .dropIndex(\"idx_reservation_expires_at_active\")\n .ifExists()\n .execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .execute();\n}\n","import { sql, type Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"client_hash\", \"text\")\n .execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"size_bytes\", \"bigint\")\n .execute();\n\n // Structural enforcement: size_bytes must be present whenever client_hash\n // is present. The expression references only the row's own columns so the\n // known withSchema() caveat (raw SQL table references are not schema-\n // qualified) does not apply here.\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addCheckConstraint(\n \"attachment_reservation_hash_size_check\",\n sql`client_hash is null or size_bytes is not null`,\n )\n .execute();\n\n // Non-unique index. Partial unique indexes are not used here: raw SQL\n // index predicates do not respect withSchema() (see migration 001), and\n // uniqueness is deliberately not a requirement (concurrent reservations\n // for the same hash are permitted -- see the hash-first design doc).\n await db.schema\n .createIndex(\"idx_reservation_client_hash\")\n .on(\"attachment_reservation\")\n .column(\"client_hash\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_client_hash\").ifExists().execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"size_bytes\")\n .execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"client_hash\")\n .execute();\n}\n","import { Migrator, sql } from \"kysely\";\nimport type { MigrationProvider, Kysely } from \"kysely\";\n\nimport * as migration001 from \"./001_create_attachment_table.js\";\nimport * as migration002 from \"./002_create_reservation_table.js\";\nimport * as migration003 from \"./003_add_reservation_expires_at.js\";\nimport * as migration004 from \"./004_add_reservation_soft_delete.js\";\nimport * as migration005 from \"./005_add_reservation_active_index.js\";\nimport * as migration006 from \"./006_add_reservation_client_hash.js\";\n\nexport const ATTACHMENT_SCHEMA = \"attachments\";\n\nexport interface MigrationResult {\n success: boolean;\n migrationsExecuted: string[];\n error?: Error;\n}\n\nconst migrations = {\n \"001_create_attachment_table\": migration001,\n \"002_create_reservation_table\": migration002,\n \"003_add_reservation_expires_at\": migration003,\n \"004_add_reservation_soft_delete\": migration004,\n \"005_add_reservation_active_index\": migration005,\n \"006_add_reservation_client_hash\": migration006,\n};\n\nclass ProgrammaticMigrationProvider implements MigrationProvider {\n getMigrations() {\n return Promise.resolve(migrations);\n }\n}\n\nexport async function runAttachmentMigrations(\n db: Kysely<any>,\n schema: string = ATTACHMENT_SCHEMA,\n): Promise<MigrationResult> {\n try {\n await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);\n } catch (error) {\n return {\n success: false,\n migrationsExecuted: [],\n error:\n error instanceof Error ? error : new Error(\"Failed to create schema\"),\n };\n }\n\n const migrator = new Migrator({\n db: db.withSchema(schema),\n provider: new ProgrammaticMigrationProvider(),\n migrationTableSchema: schema,\n });\n\n let error: unknown;\n let results: Awaited<ReturnType<typeof migrator.migrateToLatest>>[\"results\"];\n try {\n const result = await migrator.migrateToLatest();\n error = result.error;\n results = result.results;\n } catch (e) {\n error = e;\n results = [];\n }\n\n const migrationsExecuted =\n results?.map((result) => result.migrationName) ?? [];\n\n if (error) {\n return {\n success: false,\n migrationsExecuted,\n error:\n error instanceof Error ? error : new Error(\"Unknown migration error\"),\n };\n }\n\n return {\n success: true,\n migrationsExecuted,\n };\n}\n","import { mkdir, rename, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type { IAttachmentUpload, IReservationStore } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentSendOptions,\n AttachmentUploadResult,\n Reservation,\n} from \"../types.js\";\nimport type {\n AttachmentDatabase,\n AttachmentRow,\n} from \"../storage/kysely/types.js\";\nimport { HashMismatch } from \"../errors.js\";\nimport { createRef } from \"../ref.js\";\nimport {\n storageRelativePath,\n streamHashAndWrite,\n} from \"../storage/fs/attachment-fs.js\";\nimport type { AttachmentStatus } from \"../types.js\";\n\nfunction rowToHeader(row: AttachmentRow): AttachmentHeader {\n return {\n hash: row.hash,\n mimeType: row.mime_type,\n fileName: row.file_name,\n sizeBytes: Number(row.size_bytes),\n extension: row.extension,\n status: row.status as AttachmentStatus,\n source: row.source as \"local\" | \"sync\",\n createdAtUtc: row.created_at_utc,\n lastAccessedAtUtc: row.last_accessed_at_utc,\n expiresAtUtc: null,\n };\n}\n\nexport class DirectAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n readonly ref: AttachmentRef | null;\n readonly expiresAtUtc: string;\n\n constructor(\n private readonly reservation: Reservation,\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n private readonly maxBytes?: number,\n ) {\n this.reservationId = reservation.reservationId;\n this.ref =\n reservation.clientHash != null ? createRef(reservation.clientHash) : null;\n this.expiresAtUtc = reservation.expiresAtUtc;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n options?: AttachmentSendOptions,\n ): Promise<AttachmentUploadResult> {\n if (\n this.reservation.clientHash != null &&\n this.reservation.sizeBytes == null\n ) {\n throw new Error(\"hash-first reservation missing sizeBytes\");\n }\n // Stream bytes directly to a temp file while hashing. This caps memory\n // usage at one chunk regardless of payload size, and lets us enforce\n // `maxBytes` before either disk or memory grows unbounded.\n // When clientHash is present, declaredSizeBytes is enforced during the\n // stream: exceeding it aborts early, and a short stream fails at end.\n const declaredSizeBytes =\n this.reservation.clientHash != null\n ? (this.reservation.sizeBytes ?? undefined)\n : undefined;\n // The signal is honoured for the streaming write, the only part of this\n // send that can run long: a cancel there rejects and commits nothing.\n const { tempPath, hash, sizeBytes } = await streamHashAndWrite(\n this.basePath,\n data,\n {\n maxBytes: this.maxBytes,\n declaredSizeBytes,\n ...(options?.signal ? { signal: options.signal } : {}),\n },\n );\n\n // Hash verification: if the client claimed a hash, compare before any\n // DB write or rename. On mismatch the temp file is removed and the\n // reservation is deliberately retained so the client can retry.\n if (\n this.reservation.clientHash != null &&\n hash !== this.reservation.clientHash\n ) {\n await rm(tempPath, { force: true });\n throw new HashMismatch(this.reservation.clientHash, hash);\n }\n\n try {\n const existing = await this.db\n .selectFrom(\"attachment\")\n .select([\"hash\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (existing?.status === \"available\") {\n // Dedup -- bytes already on disk, drop the temp file.\n await rm(tempPath, { force: true });\n } else {\n const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await mkdir(dirname(fullPath), { recursive: true });\n await rename(tempPath, fullPath);\n\n const now = new Date().toISOString();\n\n if (!existing) {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: this.reservation.mimeType,\n file_name: this.reservation.fileName,\n size_bytes: sizeBytes,\n extension: this.reservation.extension ?? null,\n status: \"available\",\n storage_path: relPath,\n source: \"local\",\n created_at_utc: now,\n last_accessed_at_utc: now,\n })\n .onConflict((oc) => oc.column(\"hash\").doNothing())\n .execute();\n } else {\n // Existing row was evicted — restore it\n await this.db\n .updateTable(\"attachment\")\n .set({\n status: \"available\",\n storage_path: relPath,\n source: \"local\",\n last_accessed_at_utc: now,\n })\n .where(\"hash\", \"=\", hash)\n .where(\"status\", \"=\", \"evicted\")\n .execute();\n }\n }\n } catch (err) {\n await rm(tempPath, { force: true });\n throw err;\n }\n\n await this.reservations.delete(this.reservationId);\n\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirstOrThrow();\n\n return {\n hash,\n ref: createRef(hash),\n header: rowToHeader(row),\n };\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"../interfaces.js\";\nimport type { Reservation } from \"../types.js\";\nimport type { AttachmentDatabase } from \"../storage/kysely/types.js\";\nimport { DirectAttachmentUpload } from \"./direct-attachment-upload.js\";\n\nexport class DirectAttachmentUploadFactory implements IAttachmentUploadFactory {\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n private readonly maxBytes?: number,\n ) {}\n\n createUpload(reservation: Reservation): IAttachmentUpload {\n return new DirectAttachmentUpload(\n reservation,\n this.db,\n this.basePath,\n this.reservations,\n this.maxBytes,\n );\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { IAttachmentBackend, IAttachmentStore } from \"../interfaces.js\";\nimport {\n parseAttachmentDownloadTarget,\n parseAttachmentUploadTarget,\n} from \"../targets.js\";\nimport type {\n AttachmentBackendHealth,\n AttachmentDownloadTarget,\n AttachmentUploadTarget,\n Reservation,\n} from \"../types.js\";\n\nexport type FilesystemAttachmentBackendConfig = {\n uploadTarget: (reservation: Reservation) => unknown;\n downloadTarget: (hash: AttachmentHash) => unknown;\n readiness?: () => boolean | Promise<boolean>;\n};\n\n/**\n * Filesystem keeps byte transfer behind Switchboard. URL construction stays\n * at the server edge, while this adapter validates that a filesystem backend\n * can never accidentally return a direct-provider target.\n */\nexport class FilesystemAttachmentBackend implements IAttachmentBackend {\n readonly kind = \"filesystem\" as const;\n\n constructor(\n private readonly store: Pick<IAttachmentStore, \"has\">,\n private readonly config: FilesystemAttachmentBackendConfig,\n ) {}\n\n async prepareUploadTarget(\n reservation: Reservation,\n ): Promise<AttachmentUploadTarget> {\n const target = parseAttachmentUploadTarget(\n await this.config.uploadTarget(reservation),\n );\n if (target.kind !== \"switchboard\") {\n throw new Error(\"Filesystem upload target must use Switchboard\");\n }\n return target;\n }\n\n async prepareDownloadTarget(\n hash: AttachmentHash,\n ): Promise<AttachmentDownloadTarget> {\n const target = parseAttachmentDownloadTarget(\n await this.config.downloadTarget(hash),\n );\n if (target.kind !== \"switchboard\") {\n throw new Error(\"Filesystem download target must use Switchboard\");\n }\n return target;\n }\n\n exists(hash: AttachmentHash): Promise<boolean> {\n return this.store.has(hash);\n }\n\n async health(): Promise<AttachmentBackendHealth> {\n return {\n kind: this.kind,\n ready: await (this.config.readiness?.() ?? true),\n };\n }\n}\n","import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n} from \"../../interfaces.js\";\nimport { createRef } from \"../../ref.js\";\nimport type {\n AttachmentUploadResult,\n AttachmentUploadTarget,\n Reservation,\n} from \"../../types.js\";\n\nclass S3AttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n readonly ref: AttachmentRef | null;\n readonly expiresAtUtc: string;\n readonly uploadTarget?: AttachmentUploadTarget;\n\n constructor(reservation: Reservation) {\n this.reservationId = reservation.reservationId;\n this.ref =\n reservation.clientHash === null\n ? null\n : createRef(reservation.clientHash);\n this.expiresAtUtc = reservation.expiresAtUtc;\n this.uploadTarget = reservation.uploadTarget;\n }\n\n // No `options`: this handle never transfers bytes, so there is no progress\n // to report and nothing for a signal to abort. Callers must use the\n // presigned target, which honours both.\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n await data.cancel().catch(() => {});\n throw new Error(\"S3 attachment upload must use the presigned target\");\n }\n}\n\nexport class S3AttachmentUploadFactory implements IAttachmentUploadFactory {\n createUpload(reservation: Reservation): IAttachmentUpload {\n return new S3AttachmentUpload(reservation);\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\n IAttachmentBackend,\n IAttachmentTransport,\n IAttachmentUploadFactory,\n} from \"./interfaces.js\";\nimport type { AttachmentDatabase } from \"./storage/kysely/types.js\";\nimport { AttachmentService } from \"./attachment-service.js\";\nimport { KyselyAttachmentStore } from \"./storage/kysely/attachment-store.js\";\nimport { KyselyReservationStore } from \"./storage/kysely/reservation-store.js\";\nimport { DirectAttachmentUploadFactory } from \"./direct/direct-attachment-upload-factory.js\";\nimport {\n runAttachmentMigrations,\n ATTACHMENT_SCHEMA,\n} from \"./storage/migrations/migrator.js\";\nimport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\nimport { S3AttachmentUploadFactory } from \"./storage/s3/upload-factory.js\";\n\nexport type AttachmentBuildResult = {\n service: AttachmentService;\n store: KyselyAttachmentStore;\n reservations: KyselyReservationStore;\n uploadFactory: IAttachmentUploadFactory;\n /** Selected direct-transfer backend, when startup configured one. */\n backend?: IAttachmentBackend;\n /** Stops the reservation sweep timer, if one was configured via withReservationSweepMs(). */\n destroy: () => void;\n};\n\nexport class AttachmentBuilder {\n private transport: IAttachmentTransport = new NullAttachmentTransport();\n private customUploadFactory?: IAttachmentUploadFactory;\n private maxUploadBytes?: number;\n private reservationSweepMs?: number;\n private backend?: IAttachmentBackend;\n\n constructor(\n private readonly db: Kysely<any>,\n private readonly storagePath: string,\n ) {}\n\n withTransport(transport: IAttachmentTransport): this {\n this.transport = transport;\n return this;\n }\n\n withUploadFactory(factory: IAttachmentUploadFactory): this {\n this.customUploadFactory = factory;\n return this;\n }\n\n withBackend(backend: IAttachmentBackend): this {\n this.backend = backend;\n return this;\n }\n\n withMaxUploadBytes(maxBytes: number): this {\n this.maxUploadBytes = maxBytes;\n return this;\n }\n\n /**\n * Configure a recurring sweep that deletes expired reservations.\n * The sweep calls reservations.deleteExpired() on the given interval.\n * When set, the built result's destroy() clears the timer.\n * Without this option no sweep runs -- deleteExpired() is never called\n * automatically. Call withReservationSweepMs in production to prevent\n * expired reservation rows from accumulating indefinitely.\n */\n withReservationSweepMs(intervalMs: number): this {\n this.reservationSweepMs = intervalMs;\n return this;\n }\n\n async build(): Promise<AttachmentBuildResult> {\n const result = await runAttachmentMigrations(this.db, ATTACHMENT_SCHEMA);\n if (!result.success && result.error) {\n throw result.error;\n }\n\n const scopedDb = this.db.withSchema(\n ATTACHMENT_SCHEMA,\n ) as Kysely<AttachmentDatabase>;\n\n const store = new KyselyAttachmentStore(\n scopedDb,\n this.transport,\n this.storagePath,\n );\n const reservations = new KyselyReservationStore(scopedDb);\n\n const uploadFactory =\n this.customUploadFactory ??\n (this.backend?.kind === \"s3\"\n ? new S3AttachmentUploadFactory()\n : new DirectAttachmentUploadFactory(\n scopedDb,\n this.storagePath,\n reservations,\n this.maxUploadBytes,\n ));\n\n const service = new AttachmentService(\n store,\n reservations,\n uploadFactory,\n this.backend,\n );\n\n let sweepTimer: ReturnType<typeof setInterval> | undefined;\n if (this.reservationSweepMs !== undefined) {\n const intervalMs = this.reservationSweepMs;\n sweepTimer = setInterval(() => {\n // Sweep failures are retried on the next interval; swallow to prevent\n // unhandled rejection from terminating the process.\n reservations.deleteExpired().catch(() => {});\n }, intervalMs);\n if (typeof sweepTimer.unref === \"function\") {\n sweepTimer.unref();\n }\n }\n\n const destroy = (): void => {\n if (sweepTimer !== undefined) {\n clearInterval(sweepTimer);\n sweepTimer = undefined;\n }\n };\n\n return {\n service,\n store,\n reservations,\n uploadFactory,\n ...(this.backend ? { backend: this.backend } : {}),\n destroy,\n };\n }\n}\n","import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type { Kysely } from \"kysely\";\nimport { parseRef } from \"../../ref.js\";\nimport type { AttachmentReferenceDatabase } from \"./storage/types.js\";\nimport type {\n AttachmentReferenceInput,\n AttachmentReferencePageResult,\n IAttachmentReferenceReader,\n IAttachmentReferenceScanner,\n IAttachmentReferenceWriter,\n} from \"./types.js\";\n\n/** Keyset cursor: the `(document_id, attachment_ref)` pair the last page ended on. */\ntype ScanCursor = {\n documentId: string;\n ref: string;\n};\n\nfunction encodeCursor(cursor: ScanCursor): string {\n return JSON.stringify([cursor.documentId, cursor.ref]);\n}\n\nfunction decodeCursor(cursor: string): ScanCursor {\n let parsed: unknown;\n try {\n parsed = JSON.parse(cursor);\n } catch {\n // Malformed JSON is a bad cursor, not an uncaught SyntaxError leaking out\n // of a scan: report it as the named error the caller expects.\n throw new Error(\n `Invalid attachment reference scan cursor: ${JSON.stringify(cursor)}`,\n );\n }\n if (\n !Array.isArray(parsed) ||\n parsed.length !== 2 ||\n typeof parsed[0] !== \"string\" ||\n parsed[0] === \"\" ||\n typeof parsed[1] !== \"string\" ||\n parsed[1] === \"\"\n ) {\n throw new Error(\n `Invalid attachment reference scan cursor: ${JSON.stringify(cursor)}`,\n );\n }\n return { documentId: parsed[0], ref: parsed[1] };\n}\n\nexport class KyselyAttachmentReferenceStore\n implements\n IAttachmentReferenceReader,\n IAttachmentReferenceScanner,\n IAttachmentReferenceWriter\n{\n constructor(private readonly db: Kysely<AttachmentReferenceDatabase>) {}\n\n async hasReference(documentId: string, ref: AttachmentRef): Promise<boolean> {\n const row = await this.db\n .selectFrom(\"attachment_reference\")\n .select(\"document_id\")\n .where(\"document_id\", \"=\", documentId)\n .where(\"attachment_ref\", \"=\", ref)\n .executeTakeFirst();\n\n return row !== undefined;\n }\n\n async referencingScopes(\n documentId: string,\n ref: AttachmentRef,\n ): Promise<string[]> {\n const rows = await this.db\n .selectFrom(\"attachment_reference\")\n .select(\"scope\")\n .distinct()\n .where(\"document_id\", \"=\", documentId)\n .where(\"attachment_ref\", \"=\", ref)\n .execute();\n\n return rows.map((row) => row.scope);\n }\n\n /**\n * Keyset paging over the unique `(document_id, attachment_ref)` constraint,\n * rather than OFFSET: a scan that runs while the read model is still\n * indexing must not skip or repeat a row because rows appeared behind the\n * cursor. That is exactly the boot case the replicator's re-scan runs in.\n */\n async listReferences(\n cursor: string | undefined,\n limit: number,\n ): Promise<AttachmentReferencePageResult> {\n let query = this.db\n .selectFrom(\"attachment_reference\")\n .select([\"document_id\", \"attachment_ref\"])\n .orderBy(\"document_id\", \"asc\")\n .orderBy(\"attachment_ref\", \"asc\")\n .limit(limit);\n\n if (cursor !== undefined) {\n const after = decodeCursor(cursor);\n query = query.where((eb) =>\n eb.or([\n eb(\"document_id\", \">\", after.documentId),\n eb.and([\n eb(\"document_id\", \"=\", after.documentId),\n eb(\"attachment_ref\", \">\", after.ref),\n ]),\n ]),\n );\n }\n\n const rows = await query.execute();\n const last = rows.at(-1);\n return {\n references: rows.map((row) => ({\n documentId: row.document_id,\n ref: row.attachment_ref as AttachmentRef,\n })),\n nextCursor:\n rows.length === limit && last\n ? encodeCursor({\n documentId: last.document_id,\n ref: last.attachment_ref,\n })\n : undefined,\n };\n }\n\n async addReferences(\n references: readonly AttachmentReferenceInput[],\n ): Promise<void> {\n if (references.length === 0) {\n return;\n }\n\n await this.db\n .insertInto(\"attachment_reference\")\n .values(\n references.map((reference) => ({\n document_id: reference.documentId,\n attachment_ref: reference.ref,\n attachment_hash: parseRef(reference.ref).hash,\n first_operation_id: reference.operationId,\n branch: reference.branch,\n scope: reference.scope,\n first_seen_ordinal: reference.ordinal,\n created_at_utc: new Date().toISOString(),\n })),\n )\n .onConflict((oc) =>\n oc.columns([\"document_id\", \"attachment_ref\"]).doNothing(),\n )\n .execute();\n }\n\n async removeDocuments(documentIds: readonly string[]): Promise<void> {\n if (documentIds.length === 0) {\n return;\n }\n\n await this.db\n .deleteFrom(\"attachment_reference\")\n .where(\"document_id\", \"in\", [...documentIds])\n .execute();\n }\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment_reference\")\n .addColumn(\"document_id\", \"text\", (col) => col.notNull())\n .addColumn(\"attachment_ref\", \"text\", (col) => col.notNull())\n .addColumn(\"attachment_hash\", \"text\", (col) => col.notNull())\n .addColumn(\"first_operation_id\", \"text\", (col) => col.notNull())\n .addColumn(\"branch\", \"text\", (col) => col.notNull())\n .addColumn(\"scope\", \"text\", (col) => col.notNull())\n .addColumn(\"first_seen_ordinal\", \"integer\", (col) => col.notNull())\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .addUniqueConstraint(\"unique_attachment_reference_document_ref\", [\n \"document_id\",\n \"attachment_ref\",\n ])\n .execute();\n\n await db.schema\n .createIndex(\"idx_attachment_reference_ref\")\n .on(\"attachment_reference\")\n .column(\"attachment_ref\")\n .execute();\n\n await db.schema\n .createIndex(\"idx_attachment_reference_hash\")\n .on(\"attachment_reference\")\n .column(\"attachment_hash\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment_reference\").ifExists().execute();\n}\n","import { Migrator, sql } from \"kysely\";\nimport type { Kysely, MigrationProvider } from \"kysely\";\nimport * as migration001 from \"./001_create_attachment_reference_table.js\";\n\nexport const ATTACHMENT_REFERENCE_SCHEMA = \"attachment_reference_read_model\";\nexport const ATTACHMENT_REFERENCE_MIGRATION_TABLE =\n \"kysely_migration_attachment_reference_read_model\";\nexport const ATTACHMENT_REFERENCE_MIGRATION_LOCK_TABLE =\n \"kysely_migration_attachment_reference_read_model_lock\";\n\nexport interface AttachmentReferenceMigrationResult {\n success: boolean;\n migrationsExecuted: string[];\n error?: Error;\n}\n\nconst migrations = {\n \"001_create_attachment_reference_table\": migration001,\n};\n\nclass ProgrammaticMigrationProvider implements MigrationProvider {\n getMigrations() {\n return Promise.resolve(migrations);\n }\n}\n\nfunction createMigrator(db: Kysely<unknown>, schema: string): Migrator {\n return new Migrator({\n db: db.withSchema(schema),\n provider: new ProgrammaticMigrationProvider(),\n migrationTableSchema: schema,\n migrationTableName: ATTACHMENT_REFERENCE_MIGRATION_TABLE,\n migrationLockTableName: ATTACHMENT_REFERENCE_MIGRATION_LOCK_TABLE,\n });\n}\n\nfunction toResult(\n error: unknown,\n results:\n | Awaited<ReturnType<Migrator[\"migrateToLatest\"]>>[\"results\"]\n | undefined,\n): AttachmentReferenceMigrationResult {\n const migrationsExecuted =\n results?.map((result) => result.migrationName) ?? [];\n if (error) {\n return {\n success: false,\n migrationsExecuted,\n error:\n error instanceof Error ? error : new Error(\"Unknown migration error\"),\n };\n }\n return { success: true, migrationsExecuted };\n}\n\nexport async function runAttachmentReferenceMigrations(\n db: Kysely<unknown>,\n schema: string = ATTACHMENT_REFERENCE_SCHEMA,\n): Promise<AttachmentReferenceMigrationResult> {\n try {\n await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);\n } catch (error) {\n return {\n success: false,\n migrationsExecuted: [],\n error:\n error instanceof Error ? error : new Error(\"Failed to create schema\"),\n };\n }\n\n try {\n const { error, results } = await createMigrator(\n db,\n schema,\n ).migrateToLatest();\n return toResult(error, results);\n } catch (error) {\n return toResult(error, []);\n }\n}\n\nexport async function rollbackAttachmentReferenceMigration(\n db: Kysely<unknown>,\n schema: string = ATTACHMENT_REFERENCE_SCHEMA,\n): Promise<AttachmentReferenceMigrationResult> {\n try {\n const { error, results } = await createMigrator(db, schema).migrateDown();\n return toResult(error, results);\n } catch (error) {\n return toResult(error, []);\n }\n}\n\nexport async function getAttachmentReferenceMigrationStatus(\n db: Kysely<unknown>,\n schema: string = ATTACHMENT_REFERENCE_SCHEMA,\n) {\n return await createMigrator(db, schema).getMigrations();\n}\n","import type { Kysely } from \"kysely\";\nimport { KyselyAttachmentReferenceStore } from \"./kysely-attachment-reference-store.js\";\nimport type { AttachmentReferenceDatabase } from \"./storage/types.js\";\nimport {\n ATTACHMENT_REFERENCE_SCHEMA,\n runAttachmentReferenceMigrations,\n} from \"./storage/migrations/migrator.js\";\nimport type {\n IAttachmentReferenceReader,\n IAttachmentReferenceWriter,\n} from \"./types.js\";\n\nexport type AttachmentReferenceIndexBuildResult = {\n store: IAttachmentReferenceReader & IAttachmentReferenceWriter;\n};\n\nexport class AttachmentReferenceIndexBuilder {\n constructor(private readonly db: Kysely<unknown>) {}\n\n async build(): Promise<AttachmentReferenceIndexBuildResult> {\n const result = await runAttachmentReferenceMigrations(this.db);\n if (!result.success && result.error) {\n throw result.error;\n }\n\n const scopedDb = this.db.withSchema(\n ATTACHMENT_REFERENCE_SCHEMA,\n ) as Kysely<AttachmentReferenceDatabase>;\n const store = new KyselyAttachmentReferenceStore(scopedDb);\n return { store };\n }\n}\n","import {\n BaseReadModel,\n defaultReadModelIndexingConfig,\n type DocumentViewDatabase,\n type IConsistencyTracker,\n type IDocumentModelRegistry,\n type IOperationIndex,\n type IWriteCache,\n} from \"@powerhousedao/reactor\";\nimport {\n isPurgeMarker,\n type OperationWithContext,\n} from \"@powerhousedao/shared/document-model\";\nimport type { Kysely } from \"kysely\";\nimport type { IAttachmentSchemaCompiler } from \"../../reference-index/types.js\";\nimport type {\n AttachmentReferenceInput,\n IAttachmentReferenceWriter,\n} from \"./types.js\";\n\nexport const ATTACHMENT_REFERENCE_READ_MODEL_ID =\n \"attachment-reference-read-model\";\n\n/**\n * Indexes attachment references in chunks.\n *\n * Each reference row stands on its own and is written insert-or-do-nothing, so\n * a batch that commits in pieces exposes no partial structure, and re-indexing\n * the same range writes nothing new: the catch-up sweep needs no stream suffix.\n *\n * The rows live on another handle, so the purge fence is \"skip\": a tombstoned\n * id's operations are dropped at commit and its marker deletes its rows.\n */\nexport class AttachmentReferenceReadModel extends BaseReadModel {\n constructor(\n db: Kysely<DocumentViewDatabase>,\n operationIndex: IOperationIndex,\n writeCache: IWriteCache,\n consistencyTracker: IConsistencyTracker,\n private readonly documentModelRegistry: IDocumentModelRegistry,\n private readonly schemaCompiler: IAttachmentSchemaCompiler,\n private readonly referenceWriter: IAttachmentReferenceWriter,\n ) {\n super(db, operationIndex, writeCache, consistencyTracker, {\n readModelId: ATTACHMENT_REFERENCE_READ_MODEL_ID,\n rebuildStateOnInit: false,\n indexing: defaultReadModelIndexingConfig,\n replayStreamSuffix: false,\n purgeFence: \"skip\",\n });\n }\n\n protected override async commitOperations(\n items: OperationWithContext[],\n ): Promise<void> {\n const references: AttachmentReferenceInput[] = [];\n const purged = new Set<string>();\n\n for (const { operation, context } of items) {\n if (isPurgeMarker(operation)) {\n purged.add(context.documentId);\n continue;\n }\n if (operation.error !== undefined) continue;\n\n const module = this.documentModelRegistry.getModule(context.documentType);\n const extractor = this.schemaCompiler.forModuleAction(\n module,\n operation.action.type,\n );\n const refs = extractor.extract(operation.action);\n\n for (const ref of refs) {\n references.push({\n documentId: context.documentId,\n ref,\n operationId: operation.id,\n branch: context.branch,\n scope: context.scope,\n ordinal: context.ordinal,\n });\n }\n }\n\n if (references.length > 0) {\n await this.referenceWriter.addReferences(references);\n }\n if (purged.size > 0) {\n await this.referenceWriter.removeDocuments([...purged]);\n }\n }\n}\n","export const DEFAULT_S3_ATTACHMENT_PREFIX = \"attachments\";\nexport const DEFAULT_S3_UPLOAD_TTL_SECONDS = 900;\nexport const DEFAULT_S3_DOWNLOAD_TTL_SECONDS = 300;\nexport const MAX_S3_PRESIGN_TTL_SECONDS = 604_800;\n\nexport type S3AttachmentConfig = {\n endpoint: string;\n region: string;\n bucket: string;\n accessKeyId: string;\n secretAccessKey: string;\n prefix: string;\n forcePathStyle: boolean;\n uploadTtlSeconds: number;\n downloadTtlSeconds: number;\n};\n\nexport type AttachmentStorageConfig =\n | { kind: \"filesystem\" }\n | { kind: \"s3\"; s3: S3AttachmentConfig };\n\ntype Environment = Readonly<Record<string, string | undefined>>;\n\nfunction required(env: Environment, name: string): string {\n const value = env[name];\n if (value === undefined || value.trim().length === 0) {\n throw new Error(`${name} is required and must not be blank`);\n }\n if (value.trim() !== value) {\n throw new Error(`${name} must not have leading or trailing whitespace`);\n }\n return value;\n}\n\nconst LOOPBACK_HOSTNAMES = new Set([\"127.0.0.1\", \"localhost\", \"[::1]\"]);\n\n/** Plain HTTP is allowed only for loopback hosts (local S3 emulators). */\nfunction isLoopbackHost(endpoint: URL): boolean {\n return LOOPBACK_HOSTNAMES.has(endpoint.hostname.toLowerCase());\n}\n\nfunction parseEndpoint(value: string): string {\n const message =\n \"PH_ATTACHMENT_S3_ENDPOINT must be a valid HTTPS URL (HTTP is allowed only for loopback hosts)\";\n let endpoint: URL;\n try {\n endpoint = new URL(value);\n } catch {\n throw new Error(message);\n }\n const protocolAllowed =\n endpoint.protocol === \"https:\" ||\n (endpoint.protocol === \"http:\" && isLoopbackHost(endpoint));\n if (\n value.trim() !== value ||\n !protocolAllowed ||\n endpoint.username !== \"\" ||\n endpoint.password !== \"\" ||\n endpoint.search !== \"\" ||\n endpoint.hash !== \"\"\n ) {\n throw new Error(message);\n }\n return endpoint.toString().replace(/\\/$/, \"\");\n}\n\nfunction parseBoolean(\n env: Environment,\n name: string,\n defaultValue: boolean,\n): boolean {\n const value = env[name];\n if (value === undefined) return defaultValue;\n if (value === \"true\") return true;\n if (value === \"false\") return false;\n throw new Error(`${name} must be either true or false`);\n}\n\nfunction parseTtl(\n env: Environment,\n name: string,\n defaultValue: number,\n): number {\n const value = env[name];\n if (value === undefined) return defaultValue;\n if (!/^[1-9]\\d*$/.test(value)) {\n throw new Error(`${name} must be a positive integer`);\n }\n const seconds = Number(value);\n if (!Number.isSafeInteger(seconds) || seconds > MAX_S3_PRESIGN_TTL_SECONDS) {\n throw new Error(\n `${name} must be between 1 and ${MAX_S3_PRESIGN_TTL_SECONDS}`,\n );\n }\n return seconds;\n}\n\nexport function normalizeS3AttachmentPrefix(prefix: string): string {\n if (\n prefix.trim() !== prefix ||\n prefix.startsWith(\"/\") ||\n prefix.includes(\"\\\\\")\n ) {\n throw new Error(\"S3_ATTACHMENT_PREFIX is unsafe\");\n }\n const normalized = prefix.replace(/\\/+$/, \"\");\n if (normalized.length === 0) {\n throw new Error(\"S3_ATTACHMENT_PREFIX must not be blank\");\n }\n const segments = normalized.split(\"/\");\n if (\n segments.some(\n (segment) =>\n segment.length === 0 ||\n segment === \".\" ||\n segment === \"..\" ||\n !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment),\n )\n ) {\n throw new Error(\"S3_ATTACHMENT_PREFIX contains an unsafe segment\");\n }\n return segments.join(\"/\");\n}\n\nexport function parseAttachmentStorageConfig(\n env: Environment = process.env,\n): AttachmentStorageConfig {\n const selector = env.PH_ATTACHMENT_STORAGE;\n if (selector === undefined || selector === \"filesystem\") {\n return { kind: \"filesystem\" };\n }\n if (selector !== \"s3\") {\n throw new Error(\"PH_ATTACHMENT_STORAGE must be either filesystem or s3\");\n }\n\n return {\n kind: \"s3\",\n s3: {\n endpoint: parseEndpoint(required(env, \"PH_ATTACHMENT_S3_ENDPOINT\")),\n region: required(env, \"PH_ATTACHMENT_S3_REGION\"),\n bucket: required(env, \"PH_ATTACHMENT_S3_BUCKET\"),\n accessKeyId: required(env, \"PH_ATTACHMENT_S3_ACCESS_KEY_ID\"),\n secretAccessKey: required(env, \"PH_ATTACHMENT_S3_SECRET_ACCESS_KEY\"),\n prefix: normalizeS3AttachmentPrefix(\n env.S3_ATTACHMENT_PREFIX ?? DEFAULT_S3_ATTACHMENT_PREFIX,\n ),\n forcePathStyle: parseBoolean(\n env,\n \"PH_ATTACHMENT_S3_FORCE_PATH_STYLE\",\n false,\n ),\n uploadTtlSeconds: parseTtl(\n env,\n \"PH_ATTACHMENT_S3_UPLOAD_TTL_SECONDS\",\n DEFAULT_S3_UPLOAD_TTL_SECONDS,\n ),\n downloadTtlSeconds: parseTtl(\n env,\n \"PH_ATTACHMENT_S3_DOWNLOAD_TTL_SECONDS\",\n DEFAULT_S3_DOWNLOAD_TTL_SECONDS,\n ),\n },\n };\n}\n","import { Buffer } from \"node:buffer\";\n\nconst SHA256_HEX = /^[0-9a-f]{64}$/;\n\nfunction validateHash(hash: string): void {\n if (!SHA256_HEX.test(hash)) {\n throw new Error(\n \"Attachment hash must be 64 lowercase hexadecimal characters\",\n );\n }\n}\n\nexport function deriveS3AttachmentKey(hash: string, prefix: string): string {\n validateHash(hash);\n // Imported lazily at the call boundary to keep this primitive's validation\n // identical to configuration parsing without accepting unnormalized input.\n if (\n prefix.startsWith(\"/\") ||\n prefix.endsWith(\"/\") ||\n prefix.includes(\"\\\\\") ||\n prefix\n .split(\"/\")\n .some(\n (segment) =>\n segment.length === 0 ||\n segment === \".\" ||\n segment === \"..\" ||\n !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment),\n )\n ) {\n throw new Error(\"S3 attachment prefix must be normalized and safe\");\n }\n return `${prefix}/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash}`;\n}\n\nexport function sha256HexToBase64(hash: string): string {\n validateHash(hash);\n return Buffer.from(hash, \"hex\").toString(\"base64\");\n}\n","import {\n GetObjectCommand,\n HeadObjectCommand,\n PutObjectCommand,\n S3Client,\n} from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type {\n AttachmentDownloadTarget,\n AttachmentUploadTarget,\n} from \"../../types.js\";\nimport type { S3AttachmentConfig } from \"./config.js\";\nimport { deriveS3AttachmentKey, sha256HexToBase64 } from \"./keying.js\";\n\nexport interface S3CommandClient {\n send(command: object): Promise<unknown>;\n}\n\nexport type S3Presigner = (\n client: object,\n command: object,\n expiresInSeconds: number,\n) => Promise<string>;\n\ntype Dependencies = {\n client?: S3CommandClient;\n presign?: S3Presigner;\n now?: () => Date;\n};\n\nconst defaultPresigner: S3Presigner = (client, command, expiresInSeconds) =>\n getSignedUrl(client as S3Client, command as never, {\n expiresIn: expiresInSeconds,\n unhoistableHeaders: new Set([\"x-amz-checksum-sha256\"]),\n });\n\nexport class S3AttachmentPrimitives {\n readonly client: S3CommandClient;\n readonly presign: S3Presigner;\n readonly now: () => Date;\n\n constructor(\n readonly config: S3AttachmentConfig,\n dependencies: Dependencies = {},\n ) {\n this.client =\n dependencies.client ??\n new S3Client({\n endpoint: config.endpoint,\n region: config.region,\n forcePathStyle: config.forcePathStyle,\n credentials: {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n },\n });\n this.presign = dependencies.presign ?? defaultPresigner;\n this.now = dependencies.now ?? (() => new Date());\n }\n\n buildHeadObjectCommand(hash: string): object {\n return new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: deriveS3AttachmentKey(hash, this.config.prefix),\n });\n }\n\n buildPutObjectCommand(hash: string, mimeType: string): object {\n if (\n mimeType.trim().length === 0 ||\n mimeType.includes(\"\\r\") ||\n mimeType.includes(\"\\n\")\n ) {\n throw new Error(\n \"Attachment MIME type must not be blank or contain newlines\",\n );\n }\n return new PutObjectCommand({\n Bucket: this.config.bucket,\n Key: deriveS3AttachmentKey(hash, this.config.prefix),\n ContentType: mimeType,\n ChecksumSHA256: sha256HexToBase64(hash),\n });\n }\n\n buildGetObjectCommand(hash: string): object {\n return new GetObjectCommand({\n Bucket: this.config.bucket,\n Key: deriveS3AttachmentKey(hash, this.config.prefix),\n });\n }\n\n headObject(hash: string): Promise<unknown> {\n return this.client.send(this.buildHeadObjectCommand(hash));\n }\n\n async createUploadTarget(\n hash: string,\n mimeType: string,\n ttlSeconds = this.config.uploadTtlSeconds,\n ): Promise<AttachmentUploadTarget> {\n const checksum = sha256HexToBase64(hash);\n const url = await this.presign(\n this.client,\n this.buildPutObjectCommand(hash, mimeType),\n ttlSeconds,\n );\n return {\n kind: \"presigned-put\",\n method: \"PUT\",\n url,\n headers: {\n \"content-type\": mimeType,\n \"x-amz-checksum-sha256\": checksum,\n },\n expiresAtUtc: new Date(\n this.now().getTime() + ttlSeconds * 1_000,\n ).toISOString(),\n };\n }\n\n async createDownloadTarget(\n hash: string,\n ttlSeconds = this.config.downloadTtlSeconds,\n ): Promise<AttachmentDownloadTarget> {\n const url = await this.presign(\n this.client,\n this.buildGetObjectCommand(hash),\n ttlSeconds,\n );\n return {\n kind: \"presigned-get\",\n method: \"GET\",\n url,\n headers: {},\n expiresAtUtc: new Date(\n this.now().getTime() + ttlSeconds * 1_000,\n ).toISOString(),\n };\n }\n}\n\nexport function createS3AttachmentPrimitives(\n config: S3AttachmentConfig,\n dependencies: Dependencies = {},\n): S3AttachmentPrimitives {\n return new S3AttachmentPrimitives(config, dependencies);\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { Kysely } from \"kysely\";\nimport type { IAttachmentBackend } from \"../../interfaces.js\";\nimport type {\n AttachmentBackendHealth,\n AttachmentDownloadTarget,\n AttachmentUploadTarget,\n Reservation,\n} from \"../../types.js\";\nimport type { AttachmentDatabase } from \"../kysely/types.js\";\nimport type { S3AttachmentConfig } from \"./config.js\";\nimport { deriveS3AttachmentKey } from \"./keying.js\";\nimport {\n S3AttachmentPrimitives,\n type S3CommandClient,\n type S3Presigner,\n} from \"./primitives.js\";\n\nconst READINESS_PROBE_HASH = \"0\".repeat(64);\nconst OBJECT_NOT_FOUND_NAMES = new Set([\n \"NotFound\",\n \"NoSuchKey\",\n \"NoSuchObject\",\n]);\n\ntype ProviderError = {\n name?: unknown;\n code?: unknown;\n $metadata?: { httpStatusCode?: unknown };\n};\n\nexport type S3AttachmentBackendDependencies = {\n client?: S3CommandClient;\n presign?: S3Presigner;\n now?: () => Date;\n uploadTtlSeconds?: number;\n downloadTtlSeconds?: number;\n};\n\nfunction isObjectNotFound(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null) return false;\n const providerError = error as ProviderError;\n if (providerError.$metadata?.httpStatusCode !== 404) return false;\n return [providerError.name, providerError.code].some(\n (code) => typeof code === \"string\" && OBJECT_NOT_FOUND_NAMES.has(code),\n );\n}\n\nfunction requireHashFirstReservation(\n reservation: Reservation,\n): asserts reservation is Reservation & {\n clientHash: AttachmentHash;\n sizeBytes: number;\n} {\n if (reservation.clientHash === null) {\n throw new Error(\"S3 attachment reservations require a client hash\");\n }\n deriveS3AttachmentKey(reservation.clientHash, \"validation\");\n if (\n reservation.sizeBytes === null ||\n !Number.isSafeInteger(reservation.sizeBytes) ||\n reservation.sizeBytes <= 0\n ) {\n throw new Error(\n \"S3 attachment reservation sizeBytes must be a positive safe integer\",\n );\n }\n}\n\n/** Server-only S3 capability. Callers complete authorization before download. */\nexport class S3AttachmentBackend implements IAttachmentBackend {\n readonly kind = \"s3\" as const;\n private readonly primitives: S3AttachmentPrimitives;\n private readonly now: () => Date;\n private readonly uploadTtlSeconds: number;\n private readonly downloadTtlSeconds: number;\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n readonly config: S3AttachmentConfig,\n dependencies: S3AttachmentBackendDependencies = {},\n ) {\n this.primitives = new S3AttachmentPrimitives(config, dependencies);\n this.now = dependencies.now ?? (() => new Date());\n this.uploadTtlSeconds =\n dependencies.uploadTtlSeconds ?? config.uploadTtlSeconds;\n this.downloadTtlSeconds =\n dependencies.downloadTtlSeconds ?? config.downloadTtlSeconds;\n }\n\n async prepareUploadTarget(\n reservation: Reservation,\n ): Promise<AttachmentUploadTarget> {\n requireHashFirstReservation(reservation);\n const hash = reservation.clientHash;\n const now = this.now().toISOString();\n const storagePath = deriveS3AttachmentKey(hash, this.config.prefix);\n\n try {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: reservation.mimeType,\n file_name: reservation.fileName,\n size_bytes: reservation.sizeBytes,\n extension: reservation.extension,\n status: \"available\",\n storage_path: storagePath,\n source: \"local\",\n created_at_utc: reservation.createdAtUtc,\n last_accessed_at_utc: now,\n })\n .onConflict((conflict) =>\n conflict.column(\"hash\").doUpdateSet({\n mime_type: reservation.mimeType,\n file_name: reservation.fileName,\n size_bytes: reservation.sizeBytes,\n extension: reservation.extension,\n status: \"available\",\n storage_path: storagePath,\n last_accessed_at_utc: now,\n }),\n )\n .execute();\n } catch {\n throw new Error(\"S3 attachment metadata registration failed\");\n }\n\n let target: AttachmentUploadTarget;\n try {\n target = await this.primitives.createUploadTarget(\n hash,\n reservation.mimeType,\n this.uploadTtlSeconds,\n );\n } catch {\n throw new Error(\"S3 attachment upload target preparation failed\");\n }\n\n // Direct S3 completion is invisible to Switchboard; the existing sweep\n // expires this reservation normally.\n return target;\n }\n\n async prepareDownloadTarget(\n hash: AttachmentHash,\n ttlSeconds?: number,\n ): Promise<AttachmentDownloadTarget> {\n try {\n return await this.primitives.createDownloadTarget(\n hash,\n ttlSeconds ?? this.downloadTtlSeconds,\n );\n } catch {\n throw new Error(\"S3 attachment download target preparation failed\");\n }\n }\n\n async exists(hash: AttachmentHash): Promise<boolean> {\n const metadata = await this.db\n .selectFrom(\"attachment\")\n .select(\"hash\")\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n if (!metadata) return false;\n\n try {\n await this.primitives.headObject(hash);\n return true;\n } catch (error) {\n if (isObjectNotFound(error)) return false;\n // Provider errors may contain endpoints or signed request details, so the\n // raw cause intentionally must not cross this backend boundary.\n // eslint-disable-next-line preserve-caught-error\n throw new Error(\"S3 attachment existence check failed\");\n }\n }\n\n async health(): Promise<AttachmentBackendHealth> {\n try {\n await this.primitives.headObject(READINESS_PROBE_HASH);\n return { kind: this.kind, ready: true };\n } catch (error) {\n return {\n kind: this.kind,\n ready: isObjectNotFound(error),\n };\n }\n }\n}\n\nexport function createS3AttachmentBackend(\n db: Kysely<AttachmentDatabase>,\n config: S3AttachmentConfig,\n dependencies: S3AttachmentBackendDependencies = {},\n): S3AttachmentBackend {\n return new S3AttachmentBackend(db, config, dependencies);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["rowToHeader","up","down","up","down","up","down","up","down","up","down","up","down","migrations","migration001","migration002","migration003","migration004","migration005","migration006","ProgrammaticMigrationProvider","migration001","Buffer"],"sources":["../src/reserve-metadata.ts","../src/storage/fs/attachment-fs.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/001_create_attachment_table.ts","../src/storage/migrations/002_create_reservation_table.ts","../src/storage/migrations/003_add_reservation_expires_at.ts","../src/storage/migrations/004_add_reservation_soft_delete.ts","../src/storage/migrations/005_add_reservation_active_index.ts","../src/storage/migrations/006_add_reservation_client_hash.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/direct/filesystem-attachment-backend.ts","../src/storage/s3/upload-factory.ts","../src/attachment-builder.ts","../src/read-models/attachment-reference/kysely-attachment-reference-store.ts","../src/read-models/attachment-reference/storage/migrations/001_create_attachment_reference_table.ts","../src/read-models/attachment-reference/storage/migrations/migrator.ts","../src/read-models/attachment-reference/index-builder.ts","../src/read-models/attachment-reference/attachment-reference-read-model.ts","../src/storage/s3/config.ts","../src/storage/s3/keying.ts","../src/storage/s3/primitives.ts","../src/storage/s3/backend.ts"],"sourcesContent":["import { InvalidAttachmentMetadata } from \"./errors.js\";\n\n// eslint-disable-next-line no-control-regex\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f]/;\n// Characters a header value cannot carry: CTLs other than HTAB, DEL, and\n// anything above 0xFF.\n// eslint-disable-next-line no-control-regex\nconst NON_HEADER_CHARS = /[^\\t\\x20-\\x7e\\x80-\\xff]/;\n// RFC 6838 tokens, with optional `; param=value` (token or quoted-string).\nconst MIME_TYPE_PATTERN =\n /^[!#$%&'*+\\-.^_`|~\\w]+\\/[!#$%&'*+\\-.^_`|~\\w]+(?:[ \\t]*;[ \\t]*[!#$%&'*+\\-.^_`|~\\w]+=(?:[!#$%&'*+\\-.^_`|~\\w]+|\"(?:[^\"\\\\\\r\\n]|\\\\[^\\r\\n])*\"))*$/;\nconst MAX_FILENAME_LEN = 255;\nconst MAX_MIMETYPE_LEN = 255;\n\n/** The descriptive fields of a reservation, as a caller supplies them. */\nexport type ReserveMetadata = {\n mimeType: string;\n fileName: string;\n extension?: string | null;\n};\n\n/**\n * Validates reserve metadata with the rules the switchboard reserve route\n * applies.\n *\n * @throws InvalidAttachmentMetadata naming the first field that fails.\n */\nexport function validateReserveMetadata(meta: ReserveMetadata): void {\n const { mimeType, fileName, extension } = meta as Record<string, unknown>;\n if (\n typeof mimeType !== \"string\" ||\n mimeType.length === 0 ||\n mimeType.length > MAX_MIMETYPE_LEN ||\n NON_HEADER_CHARS.test(mimeType) ||\n !MIME_TYPE_PATTERN.test(mimeType)\n ) {\n throw new InvalidAttachmentMetadata(\"mimeType\");\n }\n if (\n typeof fileName !== \"string\" ||\n fileName.length === 0 ||\n fileName.length > MAX_FILENAME_LEN ||\n CONTROL_CHARS.test(fileName)\n ) {\n throw new InvalidAttachmentMetadata(\"fileName\");\n }\n if (typeof extension === \"string\") {\n if (extension.length === 0 || /[\\\\/]/.test(extension)) {\n throw new InvalidAttachmentMetadata(\"extension\");\n }\n } else if (extension !== undefined && extension !== null) {\n throw new InvalidAttachmentMetadata(\"extension\");\n }\n}\n","import { mkdir, rm, rename, access } from \"node:fs/promises\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { join, dirname } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { finished } from \"node:stream/promises\";\nimport { SizeMismatch, UploadTooLarge } from \"../../errors.js\";\n\n/**\n * Compute the absolute storage path for an attachment hash.\n * Uses a 2-level directory fan-out to avoid millions of files\n * in a single directory: ab/cd/abcdef123456...\n */\nexport function storagePath(basePath: string, hash: string): string {\n return join(basePath, storageRelativePath(hash));\n}\n\n/**\n * Compute the relative storage path for an attachment hash.\n * This is what gets stored in the database's storage_path column.\n */\nexport function storageRelativePath(hash: string): string {\n return join(hash.slice(0, 2), hash.slice(2, 4), hash);\n}\n\n/**\n * Write a ReadableStream to disk. Creates parent directories as needed.\n *\n * Bytes are streamed to a uniquely-named temp file in the destination\n * directory and only atomically renamed onto the final path once the whole\n * stream has been written and flushed. The final path therefore never holds a\n * partial or torn file: a concurrent writer of the same content-addressed\n * path (e.g. two clients re-fetching the same evicted attachment) streams to\n * its own temp file, and the rename publishes the new content all at once. On\n * any failure the temp file is removed and the previous file (if any) is left\n * untouched.\n *\n * Returns the number of bytes written.\n */\n// createWriteStream opens the file lazily, so destroy() can return before the\n// open has even happened and the open then creates the file after the unlink\n// below has run. Wait for the stream to close first, or the failed write\n// leaves its temp file behind.\nasync function discardTempFile(\n writer: ReturnType<typeof createWriteStream>,\n tempPath: string,\n): Promise<void> {\n writer.destroy();\n try {\n await finished(writer, { error: false });\n } catch {\n // The stream is already closed, which is all this wait is for.\n }\n await rm(tempPath, { force: true });\n}\n\nexport async function writeAttachmentBytes(\n path: string,\n data: ReadableStream<Uint8Array>,\n): Promise<number> {\n await mkdir(dirname(path), { recursive: true });\n\n const tempPath = join(dirname(path), `${randomUUID()}.tmp`);\n const writer = createWriteStream(tempPath);\n const reader = data.getReader();\n let bytesWritten = 0;\n let caughtError: Error | undefined;\n // A destination error can land before the drain-wait below has an 'error'\n // listener attached -- failing to open the temp file, or an async write\n // error between chunks -- so hold one listener for the whole write. First\n // error wins: an already-recorded source failure is the one to report.\n const recordError = (err: unknown) => {\n caughtError ??= err instanceof Error ? err : new Error(String(err));\n };\n writer.on(\"error\", recordError);\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n // The destination is destroyed once it errors, so writing again would\n // return false and wait for a 'drain' that can never come.\n if (caughtError) break;\n bytesWritten += value.byteLength;\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n writer.off(\"error\", onError);\n resolve();\n };\n const onError = (err: Error) => {\n writer.off(\"drain\", onDrain);\n reject(err);\n };\n writer.once(\"drain\", onDrain);\n writer.once(\"error\", onError);\n });\n }\n }\n } catch (err) {\n recordError(err);\n } finally {\n reader.releaseLock();\n }\n\n if (caughtError) {\n await discardTempFile(writer, tempPath);\n throw caughtError;\n }\n\n try {\n await new Promise<void>((resolve, reject) => {\n writer.end((err?: Error | null) => {\n if (err) reject(err);\n else resolve();\n });\n writer.once(\"error\", reject);\n });\n await rename(tempPath, path);\n } catch (err) {\n await discardTempFile(writer, tempPath);\n throw err instanceof Error ? err : new Error(String(err));\n }\n\n return bytesWritten;\n}\n\n/**\n * Open a ReadableStream from a file on disk.\n */\nexport function readAttachmentStream(path: string): ReadableStream<Uint8Array> {\n const nodeStream = createReadStream(path);\n return Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;\n}\n\n/**\n * Delete a file from disk. No-op if the file does not exist.\n */\nexport async function deleteAttachmentBytes(path: string): Promise<void> {\n await rm(path, { force: true });\n}\n\n/**\n * Check whether a file exists on disk.\n */\nexport async function attachmentBytesExist(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create a ReadableStream from an in-memory buffer.\n */\nexport function streamFromBuffer(data: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(data);\n controller.close();\n },\n });\n}\n\n/**\n * Stream bytes to a temp file under `${basePath}/.tmp/` while computing the\n * SHA-256 hash, returning the temp path, hex hash, and total bytes written.\n *\n * Bytes are never buffered in memory beyond the current chunk. The caller is\n * responsible for renaming the temp file to its final hash-derived location\n * (or removing it if the content is a duplicate).\n *\n * If `maxBytes` is set and the input exceeds it, the temp file is removed and\n * `UploadTooLarge` is thrown.\n *\n * If `declaredSizeBytes` is set, the byte count is enforced as a contract:\n * mid-stream, the moment the count exceeds the declaration the reader is\n * released and `SizeMismatch` is thrown without consuming the rest of the\n * stream. At stream end, if the count does not equal the declaration,\n * `SizeMismatch` is thrown. Both the `maxBytes` and `declaredSizeBytes`\n * checks apply; `maxBytes` is evaluated first on each chunk.\n *\n * If `signal` aborts, the temp file is removed and the signal's reason is\n * thrown. Nothing partial is ever returned, so a cancelled transfer cannot be\n * committed.\n */\nexport async function streamHashAndWrite(\n basePath: string,\n data: ReadableStream<Uint8Array>,\n options: {\n maxBytes?: number;\n declaredSizeBytes?: number;\n signal?: AbortSignal;\n } = {},\n): Promise<{ tempPath: string; hash: string; sizeBytes: number }> {\n const { maxBytes, declaredSizeBytes, signal } = options;\n // Before the temp file exists: an already-aborted transfer leaves no trace.\n signal?.throwIfAborted();\n const tmpDir = join(basePath, \".tmp\");\n await mkdir(tmpDir, { recursive: true });\n const tempPath = join(tmpDir, randomUUID());\n\n const hasher = createHash(\"sha256\");\n const writer = createWriteStream(tempPath);\n const reader = data.getReader();\n // Cancelling the source is what makes an abort observable while a read is\n // stalled -- it resolves the pending read as `done`. The post-loop\n // `throwIfAborted` is what stops that resolution being mistaken for the end\n // of the stream, which would commit a truncated attachment.\n const onAbort = () => {\n reader.cancel(signal?.reason).catch(() => {});\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n let sizeBytes = 0;\n let caughtError: Error | undefined;\n\n try {\n for (;;) {\n signal?.throwIfAborted();\n const { done, value } = await reader.read();\n if (done) break;\n sizeBytes += value.byteLength;\n if (maxBytes !== undefined && sizeBytes > maxBytes) {\n throw new UploadTooLarge(maxBytes);\n }\n if (declaredSizeBytes !== undefined && sizeBytes > declaredSizeBytes) {\n throw new SizeMismatch(declaredSizeBytes, sizeBytes);\n }\n hasher.update(value);\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n writer.off(\"error\", onError);\n resolve();\n };\n const onError = (err: Error) => {\n writer.off(\"drain\", onDrain);\n reject(err);\n };\n writer.once(\"drain\", onDrain);\n writer.once(\"error\", onError);\n });\n }\n }\n signal?.throwIfAborted();\n } catch (err) {\n caughtError = err instanceof Error ? err : new Error(String(err));\n } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n reader.releaseLock();\n }\n\n let endError: Error | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n writer.end((err?: Error | null) => {\n if (err) reject(err);\n else resolve();\n });\n writer.once(\"error\", reject);\n });\n } catch (err) {\n endError = err instanceof Error ? err : new Error(String(err));\n }\n\n if (caughtError) {\n await rm(tempPath, { force: true });\n throw caughtError;\n }\n if (endError) {\n await rm(tempPath, { force: true });\n throw endError;\n }\n\n if (declaredSizeBytes !== undefined && sizeBytes !== declaredSizeBytes) {\n await rm(tempPath, { force: true });\n throw new SizeMismatch(declaredSizeBytes, sizeBytes);\n }\n\n return {\n tempPath,\n hash: hasher.digest(\"hex\"),\n sizeBytes,\n };\n}\n","import { join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\nimport type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentStore,\n IAttachmentTransport,\n} from \"../../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentStatus,\n} from \"../../types.js\";\nimport { AttachmentNotFound, AttachmentPending } from \"../../errors.js\";\nimport type { AttachmentDatabase, AttachmentRow } from \"./types.js\";\nimport {\n storageRelativePath,\n writeAttachmentBytes,\n readAttachmentStream,\n deleteAttachmentBytes,\n} from \"../fs/attachment-fs.js\";\n\nfunction rowToHeader(row: AttachmentRow): AttachmentHeader {\n return {\n hash: row.hash,\n mimeType: row.mime_type,\n fileName: row.file_name,\n sizeBytes: Number(row.size_bytes),\n extension: row.extension,\n status: row.status as AttachmentStatus,\n source: row.source as \"local\" | \"sync\",\n createdAtUtc: row.created_at_utc,\n lastAccessedAtUtc: row.last_accessed_at_utc,\n expiresAtUtc: null,\n };\n}\n\nfunction wrapStreamWithCleanup(\n source: ReadableStream<Uint8Array>,\n cleanup: () => void,\n): ReadableStream<Uint8Array> {\n let cleaned = false;\n const doCleanup = () => {\n if (!cleaned) {\n cleaned = true;\n cleanup();\n }\n };\n\n const reader = source.getReader();\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) {\n doCleanup();\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (err) {\n doCleanup();\n controller.error(err);\n }\n },\n cancel() {\n doCleanup();\n reader.cancel().catch(() => {});\n },\n });\n}\n\nexport class KyselyAttachmentStore implements IAttachmentStore {\n private readonly activeReaders = new Map<string, number>();\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly transport: IAttachmentTransport,\n private readonly basePath: string,\n ) {}\n\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (row) {\n return rowToHeader(row);\n }\n\n const now = new Date().toISOString();\n const pending = await this.findPendingReservation(hash, now);\n\n if (pending) {\n return {\n hash,\n mimeType: pending.mime_type,\n fileName: pending.file_name,\n sizeBytes: Number(pending.size_bytes),\n extension: pending.extension,\n status: \"pending\",\n source: \"local\",\n createdAtUtc: pending.created_at_utc,\n lastAccessedAtUtc: pending.created_at_utc,\n expiresAtUtc: pending.expires_at_utc,\n };\n }\n\n throw new AttachmentNotFound(hash);\n }\n\n async has(hash: AttachmentHash): Promise<boolean> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .select(\"status\")\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n return row?.status === \"available\";\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n documentId?: string,\n ): Promise<AttachmentResponse> {\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (row) {\n if (row.status === \"evicted\") {\n return this.fetchRemote(hash, signal, documentId);\n }\n\n const now = new Date().toISOString();\n await this.db\n .updateTable(\"attachment\")\n .set({ last_accessed_at_utc: now })\n .where(\"hash\", \"=\", hash)\n .execute();\n\n const header = rowToHeader(row);\n header.lastAccessedAtUtc = now;\n\n this.acquireReader(hash);\n\n const fullPath = join(this.basePath, row.storage_path);\n const rawStream = readAttachmentStream(fullPath);\n const body = wrapStreamWithCleanup(rawStream, () =>\n this.releaseReader(hash),\n );\n\n return { header, body };\n }\n\n const now = new Date().toISOString();\n const pending = await this.findPendingReservation(hash, now);\n\n if (pending) {\n throw new AttachmentPending(hash, pending.expires_at_utc, {\n mimeType: pending.mime_type,\n fileName: pending.file_name,\n sizeBytes: pending.size_bytes,\n });\n }\n\n return this.fetchRemote(hash, signal, documentId);\n }\n\n async put(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const existing = await this.db\n .selectFrom(\"attachment\")\n .select([\"hash\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (existing?.status === \"available\") {\n await data.cancel();\n return;\n }\n\n const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await writeAttachmentBytes(fullPath, data);\n\n const now = new Date().toISOString();\n\n if (!existing) {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: metadata.mimeType,\n file_name: metadata.fileName,\n size_bytes: metadata.sizeBytes,\n extension: metadata.extension ?? null,\n status: \"available\",\n storage_path: relPath,\n source: \"sync\",\n created_at_utc: metadata.createdAtUtc,\n last_accessed_at_utc: now,\n })\n .onConflict((oc) => oc.column(\"hash\").doNothing())\n .execute();\n } else {\n await this.db\n .updateTable(\"attachment\")\n .set({\n status: \"available\",\n storage_path: relPath,\n last_accessed_at_utc: now,\n })\n .where(\"hash\", \"=\", hash)\n .where(\"status\", \"=\", \"evicted\")\n .execute();\n }\n }\n\n async evict(hash: AttachmentHash): Promise<void> {\n if (this.hasActiveReaders(hash)) {\n return;\n }\n\n const row = await this.db\n .selectFrom(\"attachment\")\n .select([\"storage_path\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (!row || row.status === \"evicted\") {\n return;\n }\n\n const fullPath = join(this.basePath, row.storage_path);\n await deleteAttachmentBytes(fullPath);\n\n await this.db\n .updateTable(\"attachment\")\n .set({ status: \"evicted\" })\n .where(\"hash\", \"=\", hash)\n .execute();\n }\n\n async storageUsed(): Promise<number> {\n const result = await this.db\n .selectFrom(\"attachment\")\n .select(sql<string>`COALESCE(SUM(size_bytes), 0)`.as(\"total\"))\n .where(\"status\", \"=\", \"available\")\n .executeTakeFirst();\n\n return Number(result?.total ?? 0);\n }\n\n // Private: remote re-fetch, pending reservation lookup and active reader tracking\n\n private async fetchRemote(\n hash: AttachmentHash,\n signal: AbortSignal | undefined,\n documentId: string | undefined,\n ): Promise<AttachmentResponse> {\n if (documentId === undefined) {\n throw new AttachmentNotFound(hash);\n }\n const remote = await this.transport.fetch(hash, documentId, signal);\n if (remote.kind === \"data\") {\n await this.put(hash, remote.response.metadata, remote.response.body);\n return this.get(hash, signal);\n }\n if (remote.kind === \"pending\") {\n throw new AttachmentPending(hash, remote.expiresAtUtc);\n }\n throw new AttachmentNotFound(hash);\n }\n\n private async findPendingReservation(\n hash: AttachmentHash,\n now: string,\n ): Promise<{\n mime_type: string;\n file_name: string;\n extension: string | null;\n size_bytes: number;\n created_at_utc: string;\n expires_at_utc: string;\n } | null> {\n const row = await this.db\n .selectFrom(\"attachment_reservation as r\")\n .leftJoin(\"attachment as a\", \"a.hash\", \"r.client_hash\")\n .select([\n \"r.mime_type\",\n \"r.file_name\",\n \"r.extension\",\n \"r.size_bytes\",\n \"r.created_at_utc\",\n \"r.expires_at_utc\",\n ])\n .where(\"r.client_hash\", \"=\", hash)\n .where(\"r.deleted_at_utc\", \"is\", null)\n .where(\"r.expires_at_utc\", \">\", now)\n .where(\"r.size_bytes\", \"is not\", null)\n .where(\"a.hash\", \"is\", null)\n .orderBy(\"r.expires_at_utc\", \"desc\")\n .executeTakeFirst();\n\n if (!row) return null;\n return {\n mime_type: row.mime_type,\n file_name: row.file_name,\n extension: row.extension,\n size_bytes: Number(row.size_bytes),\n created_at_utc: row.created_at_utc,\n expires_at_utc: row.expires_at_utc,\n };\n }\n\n private acquireReader(hash: string): void {\n this.activeReaders.set(hash, (this.activeReaders.get(hash) ?? 0) + 1);\n }\n\n private releaseReader(hash: string): void {\n const count = (this.activeReaders.get(hash) ?? 1) - 1;\n if (count <= 0) {\n this.activeReaders.delete(hash);\n } else {\n this.activeReaders.set(hash, count);\n }\n }\n\n private hasActiveReaders(hash: string): boolean {\n return (this.activeReaders.get(hash) ?? 0) > 0;\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { Kysely } from \"kysely\";\nimport type { IReservationStore } from \"../../interfaces.js\";\nimport type { Reservation, ReserveAttachmentOptions } from \"../../types.js\";\nimport { ReservationNotFound } from \"../../errors.js\";\nimport type { AttachmentDatabase, ReservationRow } from \"./types.js\";\n\nexport const DEFAULT_RESERVATION_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction rowToReservation(row: ReservationRow): Reservation {\n return {\n reservationId: row.reservation_id,\n mimeType: row.mime_type,\n fileName: row.file_name,\n extension: row.extension,\n createdAtUtc: row.created_at_utc,\n expiresAtUtc: row.expires_at_utc,\n clientHash: row.client_hash,\n sizeBytes: row.size_bytes !== null ? Number(row.size_bytes) : null,\n };\n}\n\nexport class KyselyReservationStore implements IReservationStore {\n private readonly ttlMs: number;\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n ttlMs: number = DEFAULT_RESERVATION_TTL_MS,\n ) {\n this.ttlMs = ttlMs;\n }\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const reservationId = randomUUID();\n const nowMs = Date.now();\n const now = new Date(nowMs).toISOString();\n const expiresAt = new Date(nowMs + this.ttlMs).toISOString();\n\n const row = await this.db\n .insertInto(\"attachment_reservation\")\n .values({\n reservation_id: reservationId,\n mime_type: options.mimeType,\n file_name: options.fileName,\n extension: options.extension ?? null,\n created_at_utc: now,\n expires_at_utc: expiresAt,\n client_hash: options.clientHash ?? null,\n size_bytes: options.sizeBytes ?? null,\n })\n .returningAll()\n .executeTakeFirstOrThrow();\n\n return rowToReservation(row);\n }\n\n async get(reservationId: string): Promise<Reservation> {\n const row = await this.db\n .selectFrom(\"attachment_reservation\")\n .selectAll()\n .where(\"reservation_id\", \"=\", reservationId)\n .where(\"deleted_at_utc\", \"is\", null)\n .executeTakeFirst();\n\n if (!row) {\n throw new ReservationNotFound(reservationId);\n }\n\n return rowToReservation(row);\n }\n\n async delete(reservationId: string): Promise<void> {\n await this.db\n .updateTable(\"attachment_reservation\")\n .set({ deleted_at_utc: new Date().toISOString() })\n .where(\"reservation_id\", \"=\", reservationId)\n .where(\"deleted_at_utc\", \"is\", null)\n .execute();\n }\n\n async deleteExpired(now: Date = new Date()): Promise<number> {\n const nowIso = now.toISOString();\n const result = await this.db\n .updateTable(\"attachment_reservation\")\n .set({ deleted_at_utc: nowIso })\n .where(\"expires_at_utc\", \"<=\", nowIso)\n .where(\"deleted_at_utc\", \"is\", null)\n .executeTakeFirst();\n\n return Number(result.numUpdatedRows);\n }\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment\")\n .addColumn(\"hash\", \"text\", (col) => col.primaryKey())\n .addColumn(\"mime_type\", \"text\", (col) => col.notNull())\n .addColumn(\"file_name\", \"text\", (col) => col.notNull())\n .addColumn(\"size_bytes\", \"bigint\", (col) => col.notNull())\n .addColumn(\"extension\", \"text\")\n .addColumn(\"status\", \"text\", (col) => col.notNull().defaultTo(\"available\"))\n .addColumn(\"storage_path\", \"text\", (col) => col.notNull())\n .addColumn(\"source\", \"text\", (col) => col.notNull().defaultTo(\"local\"))\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .addColumn(\"last_accessed_at_utc\", \"text\", (col) => col.notNull())\n .execute();\n\n await db.schema\n .createIndex(\"idx_attachment_status\")\n .on(\"attachment\")\n .column(\"status\")\n .execute();\n\n // Compound index serves the LRU eviction query:\n // SELECT ... WHERE status = 'available' ORDER BY last_accessed_at_utc ASC\n // A partial index would be ideal but raw SQL doesn't respect withSchema().\n await db.schema\n .createIndex(\"idx_attachment_lru\")\n .on(\"attachment\")\n .columns([\"status\", \"last_accessed_at_utc\"])\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment\").ifExists().execute();\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment_reservation\")\n .addColumn(\"reservation_id\", \"text\", (col) => col.primaryKey())\n .addColumn(\"mime_type\", \"text\", (col) => col.notNull())\n .addColumn(\"file_name\", \"text\", (col) => col.notNull())\n .addColumn(\"extension\", \"text\")\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment_reservation\").ifExists().execute();\n}\n","import { sql, type Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"expires_at_utc\", \"text\")\n .execute();\n\n await db\n .updateTable(\"attachment_reservation\")\n .set({ expires_at_utc: sql`created_at_utc` })\n .where(\"expires_at_utc\", \"is\", null)\n .execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .alterColumn(\"expires_at_utc\", (col) => col.setNotNull())\n .execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_expires_at\").ifExists().execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"expires_at_utc\")\n .execute();\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"deleted_at_utc\", \"text\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"deleted_at_utc\")\n .execute();\n}\n","import { sql, type Kysely, type SqlBool } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_expires_at\").ifExists().execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at_active\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .where(sql<SqlBool>`deleted_at_utc IS NULL`)\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema\n .dropIndex(\"idx_reservation_expires_at_active\")\n .ifExists()\n .execute();\n\n await db.schema\n .createIndex(\"idx_reservation_expires_at\")\n .on(\"attachment_reservation\")\n .column(\"expires_at_utc\")\n .execute();\n}\n","import { sql, type Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"client_hash\", \"text\")\n .execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addColumn(\"size_bytes\", \"bigint\")\n .execute();\n\n // Structural enforcement: size_bytes must be present whenever client_hash\n // is present. The expression references only the row's own columns so the\n // known withSchema() caveat (raw SQL table references are not schema-\n // qualified) does not apply here.\n await db.schema\n .alterTable(\"attachment_reservation\")\n .addCheckConstraint(\n \"attachment_reservation_hash_size_check\",\n sql`client_hash is null or size_bytes is not null`,\n )\n .execute();\n\n // Non-unique index. Partial unique indexes are not used here: raw SQL\n // index predicates do not respect withSchema() (see migration 001), and\n // uniqueness is deliberately not a requirement (concurrent reservations\n // for the same hash are permitted -- see the hash-first design doc).\n await db.schema\n .createIndex(\"idx_reservation_client_hash\")\n .on(\"attachment_reservation\")\n .column(\"client_hash\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropIndex(\"idx_reservation_client_hash\").ifExists().execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"size_bytes\")\n .execute();\n\n await db.schema\n .alterTable(\"attachment_reservation\")\n .dropColumn(\"client_hash\")\n .execute();\n}\n","import { Migrator, sql } from \"kysely\";\nimport type { MigrationProvider, Kysely } from \"kysely\";\n\nimport * as migration001 from \"./001_create_attachment_table.js\";\nimport * as migration002 from \"./002_create_reservation_table.js\";\nimport * as migration003 from \"./003_add_reservation_expires_at.js\";\nimport * as migration004 from \"./004_add_reservation_soft_delete.js\";\nimport * as migration005 from \"./005_add_reservation_active_index.js\";\nimport * as migration006 from \"./006_add_reservation_client_hash.js\";\n\nexport const ATTACHMENT_SCHEMA = \"attachments\";\n\nexport interface MigrationResult {\n success: boolean;\n migrationsExecuted: string[];\n error?: Error;\n}\n\nconst migrations = {\n \"001_create_attachment_table\": migration001,\n \"002_create_reservation_table\": migration002,\n \"003_add_reservation_expires_at\": migration003,\n \"004_add_reservation_soft_delete\": migration004,\n \"005_add_reservation_active_index\": migration005,\n \"006_add_reservation_client_hash\": migration006,\n};\n\nclass ProgrammaticMigrationProvider implements MigrationProvider {\n getMigrations() {\n return Promise.resolve(migrations);\n }\n}\n\nexport async function runAttachmentMigrations(\n db: Kysely<any>,\n schema: string = ATTACHMENT_SCHEMA,\n): Promise<MigrationResult> {\n try {\n await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);\n } catch (error) {\n return {\n success: false,\n migrationsExecuted: [],\n error:\n error instanceof Error ? error : new Error(\"Failed to create schema\"),\n };\n }\n\n const migrator = new Migrator({\n db: db.withSchema(schema),\n provider: new ProgrammaticMigrationProvider(),\n migrationTableSchema: schema,\n });\n\n let error: unknown;\n let results: Awaited<ReturnType<typeof migrator.migrateToLatest>>[\"results\"];\n try {\n const result = await migrator.migrateToLatest();\n error = result.error;\n results = result.results;\n } catch (e) {\n error = e;\n results = [];\n }\n\n const migrationsExecuted =\n results?.map((result) => result.migrationName) ?? [];\n\n if (error) {\n return {\n success: false,\n migrationsExecuted,\n error:\n error instanceof Error ? error : new Error(\"Unknown migration error\"),\n };\n }\n\n return {\n success: true,\n migrationsExecuted,\n };\n}\n","import { mkdir, rename, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type { IAttachmentUpload, IReservationStore } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentSendOptions,\n AttachmentUploadResult,\n Reservation,\n} from \"../types.js\";\nimport type {\n AttachmentDatabase,\n AttachmentRow,\n} from \"../storage/kysely/types.js\";\nimport { HashMismatch } from \"../errors.js\";\nimport { createRef } from \"../ref.js\";\nimport {\n storageRelativePath,\n streamHashAndWrite,\n} from \"../storage/fs/attachment-fs.js\";\nimport type { AttachmentStatus } from \"../types.js\";\nimport { isBlob } from \"../blob.js\";\n\nfunction rowToHeader(row: AttachmentRow): AttachmentHeader {\n return {\n hash: row.hash,\n mimeType: row.mime_type,\n fileName: row.file_name,\n sizeBytes: Number(row.size_bytes),\n extension: row.extension,\n status: row.status as AttachmentStatus,\n source: row.source as \"local\" | \"sync\",\n createdAtUtc: row.created_at_utc,\n lastAccessedAtUtc: row.last_accessed_at_utc,\n expiresAtUtc: null,\n };\n}\n\nexport class DirectAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n readonly ref: AttachmentRef | null;\n readonly expiresAtUtc: string;\n\n constructor(\n private readonly reservation: Reservation,\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n private readonly maxBytes?: number,\n ) {\n this.reservationId = reservation.reservationId;\n this.ref =\n reservation.clientHash != null ? createRef(reservation.clientHash) : null;\n this.expiresAtUtc = reservation.expiresAtUtc;\n }\n\n async send(\n data: ReadableStream<Uint8Array> | Blob,\n options?: AttachmentSendOptions,\n ): Promise<AttachmentUploadResult> {\n if (\n this.reservation.clientHash != null &&\n this.reservation.sizeBytes == null\n ) {\n throw new Error(\"hash-first reservation missing sizeBytes\");\n }\n // Stream bytes directly to a temp file while hashing. This caps memory\n // usage at one chunk regardless of payload size, and lets us enforce\n // `maxBytes` before either disk or memory grows unbounded.\n // When clientHash is present, declaredSizeBytes is enforced during the\n // stream: exceeding it aborts early, and a short stream fails at end.\n const declaredSizeBytes =\n this.reservation.clientHash != null\n ? (this.reservation.sizeBytes ?? undefined)\n : undefined;\n // The signal is honoured for the streaming write, the only part of this\n // send that can run long: a cancel there rejects and commits nothing.\n const { tempPath, hash, sizeBytes } = await streamHashAndWrite(\n this.basePath,\n isBlob(data) ? data.stream() : data,\n {\n maxBytes: this.maxBytes,\n declaredSizeBytes,\n ...(options?.signal ? { signal: options.signal } : {}),\n },\n );\n\n // Hash verification: if the client claimed a hash, compare before any\n // DB write or rename. On mismatch the temp file is removed and the\n // reservation is deliberately retained so the client can retry.\n if (\n this.reservation.clientHash != null &&\n hash !== this.reservation.clientHash\n ) {\n await rm(tempPath, { force: true });\n throw new HashMismatch(this.reservation.clientHash, hash);\n }\n\n try {\n const existing = await this.db\n .selectFrom(\"attachment\")\n .select([\"hash\", \"status\"])\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n\n if (existing?.status === \"available\") {\n // Dedup -- bytes already on disk, drop the temp file.\n await rm(tempPath, { force: true });\n } else {\n const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await mkdir(dirname(fullPath), { recursive: true });\n await rename(tempPath, fullPath);\n\n const now = new Date().toISOString();\n\n if (!existing) {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: this.reservation.mimeType,\n file_name: this.reservation.fileName,\n size_bytes: sizeBytes,\n extension: this.reservation.extension ?? null,\n status: \"available\",\n storage_path: relPath,\n source: \"local\",\n created_at_utc: now,\n last_accessed_at_utc: now,\n })\n .onConflict((oc) => oc.column(\"hash\").doNothing())\n .execute();\n } else {\n // Existing row was evicted — restore it\n await this.db\n .updateTable(\"attachment\")\n .set({\n status: \"available\",\n storage_path: relPath,\n source: \"local\",\n last_accessed_at_utc: now,\n })\n .where(\"hash\", \"=\", hash)\n .where(\"status\", \"=\", \"evicted\")\n .execute();\n }\n }\n } catch (err) {\n await rm(tempPath, { force: true });\n throw err;\n }\n\n await this.reservations.delete(this.reservationId);\n\n const row = await this.db\n .selectFrom(\"attachment\")\n .selectAll()\n .where(\"hash\", \"=\", hash)\n .executeTakeFirstOrThrow();\n\n return {\n hash,\n ref: createRef(hash),\n header: rowToHeader(row),\n };\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"../interfaces.js\";\nimport type { Reservation } from \"../types.js\";\nimport type { AttachmentDatabase } from \"../storage/kysely/types.js\";\nimport { DirectAttachmentUpload } from \"./direct-attachment-upload.js\";\n\nexport class DirectAttachmentUploadFactory implements IAttachmentUploadFactory {\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n private readonly maxBytes?: number,\n ) {}\n\n createUpload(reservation: Reservation): IAttachmentUpload {\n return new DirectAttachmentUpload(\n reservation,\n this.db,\n this.basePath,\n this.reservations,\n this.maxBytes,\n );\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { IAttachmentBackend, IAttachmentStore } from \"../interfaces.js\";\nimport {\n parseAttachmentDownloadTarget,\n parseAttachmentUploadTarget,\n} from \"../targets.js\";\nimport type {\n AttachmentBackendHealth,\n AttachmentDownloadTarget,\n AttachmentUploadTarget,\n Reservation,\n} from \"../types.js\";\n\nexport type FilesystemAttachmentBackendConfig = {\n uploadTarget: (reservation: Reservation) => unknown;\n downloadTarget: (hash: AttachmentHash) => unknown;\n readiness?: () => boolean | Promise<boolean>;\n};\n\n/**\n * Filesystem keeps byte transfer behind Switchboard. URL construction stays\n * at the server edge, while this adapter validates that a filesystem backend\n * can never accidentally return a direct-provider target.\n */\nexport class FilesystemAttachmentBackend implements IAttachmentBackend {\n readonly kind = \"filesystem\" as const;\n\n constructor(\n private readonly store: Pick<IAttachmentStore, \"has\">,\n private readonly config: FilesystemAttachmentBackendConfig,\n ) {}\n\n async prepareUploadTarget(\n reservation: Reservation,\n ): Promise<AttachmentUploadTarget> {\n const target = parseAttachmentUploadTarget(\n await this.config.uploadTarget(reservation),\n );\n if (target.kind !== \"switchboard\") {\n throw new Error(\"Filesystem upload target must use Switchboard\");\n }\n return target;\n }\n\n async prepareDownloadTarget(\n hash: AttachmentHash,\n ): Promise<AttachmentDownloadTarget> {\n const target = parseAttachmentDownloadTarget(\n await this.config.downloadTarget(hash),\n );\n if (target.kind !== \"switchboard\") {\n throw new Error(\"Filesystem download target must use Switchboard\");\n }\n return target;\n }\n\n exists(hash: AttachmentHash): Promise<boolean> {\n return this.store.has(hash);\n }\n\n async health(): Promise<AttachmentBackendHealth> {\n return {\n kind: this.kind,\n ready: await (this.config.readiness?.() ?? true),\n };\n }\n}\n","import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n} from \"../../interfaces.js\";\nimport { createRef } from \"../../ref.js\";\nimport type {\n AttachmentUploadResult,\n AttachmentUploadTarget,\n Reservation,\n} from \"../../types.js\";\nimport { isBlob } from \"../../blob.js\";\n\nclass S3AttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n readonly ref: AttachmentRef | null;\n readonly expiresAtUtc: string;\n readonly uploadTarget?: AttachmentUploadTarget;\n\n constructor(reservation: Reservation) {\n this.reservationId = reservation.reservationId;\n this.ref =\n reservation.clientHash === null\n ? null\n : createRef(reservation.clientHash);\n this.expiresAtUtc = reservation.expiresAtUtc;\n this.uploadTarget = reservation.uploadTarget;\n }\n\n // No `options`: this handle never transfers bytes, so there is no progress\n // to report and nothing for a signal to abort. Callers must use the\n // presigned target, which honours both.\n async send(\n data: ReadableStream<Uint8Array> | Blob,\n ): Promise<AttachmentUploadResult> {\n if (!isBlob(data)) await data.cancel().catch(() => {});\n throw new Error(\"S3 attachment upload must use the presigned target\");\n }\n}\n\nexport class S3AttachmentUploadFactory implements IAttachmentUploadFactory {\n createUpload(reservation: Reservation): IAttachmentUpload {\n return new S3AttachmentUpload(reservation);\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\n IAttachmentBackend,\n IAttachmentTransport,\n IAttachmentUploadFactory,\n} from \"./interfaces.js\";\nimport type { AttachmentDatabase } from \"./storage/kysely/types.js\";\nimport { AttachmentService } from \"./attachment-service.js\";\nimport { KyselyAttachmentStore } from \"./storage/kysely/attachment-store.js\";\nimport { KyselyReservationStore } from \"./storage/kysely/reservation-store.js\";\nimport { DirectAttachmentUploadFactory } from \"./direct/direct-attachment-upload-factory.js\";\nimport {\n runAttachmentMigrations,\n ATTACHMENT_SCHEMA,\n} from \"./storage/migrations/migrator.js\";\nimport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\nimport { S3AttachmentUploadFactory } from \"./storage/s3/upload-factory.js\";\n\nexport type AttachmentBuildResult = {\n service: AttachmentService;\n store: KyselyAttachmentStore;\n reservations: KyselyReservationStore;\n uploadFactory: IAttachmentUploadFactory;\n /** Selected direct-transfer backend, when startup configured one. */\n backend?: IAttachmentBackend;\n /** Stops the reservation sweep timer, if one was configured via withReservationSweepMs(). */\n destroy: () => void;\n};\n\nexport class AttachmentBuilder {\n private transport: IAttachmentTransport = new NullAttachmentTransport();\n private customUploadFactory?: IAttachmentUploadFactory;\n private maxUploadBytes?: number;\n private reservationSweepMs?: number;\n private backend?: IAttachmentBackend;\n\n constructor(\n private readonly db: Kysely<any>,\n private readonly storagePath: string,\n ) {}\n\n withTransport(transport: IAttachmentTransport): this {\n this.transport = transport;\n return this;\n }\n\n withUploadFactory(factory: IAttachmentUploadFactory): this {\n this.customUploadFactory = factory;\n return this;\n }\n\n withBackend(backend: IAttachmentBackend): this {\n this.backend = backend;\n return this;\n }\n\n withMaxUploadBytes(maxBytes: number): this {\n this.maxUploadBytes = maxBytes;\n return this;\n }\n\n /**\n * Configure a recurring sweep that deletes expired reservations.\n * The sweep calls reservations.deleteExpired() on the given interval.\n * When set, the built result's destroy() clears the timer.\n * Without this option no sweep runs -- deleteExpired() is never called\n * automatically. Call withReservationSweepMs in production to prevent\n * expired reservation rows from accumulating indefinitely.\n */\n withReservationSweepMs(intervalMs: number): this {\n this.reservationSweepMs = intervalMs;\n return this;\n }\n\n async build(): Promise<AttachmentBuildResult> {\n const result = await runAttachmentMigrations(this.db, ATTACHMENT_SCHEMA);\n if (!result.success && result.error) {\n throw result.error;\n }\n\n const scopedDb = this.db.withSchema(\n ATTACHMENT_SCHEMA,\n ) as Kysely<AttachmentDatabase>;\n\n const store = new KyselyAttachmentStore(\n scopedDb,\n this.transport,\n this.storagePath,\n );\n const reservations = new KyselyReservationStore(scopedDb);\n\n const uploadFactory =\n this.customUploadFactory ??\n (this.backend?.kind === \"s3\"\n ? new S3AttachmentUploadFactory()\n : new DirectAttachmentUploadFactory(\n scopedDb,\n this.storagePath,\n reservations,\n this.maxUploadBytes,\n ));\n\n const service = new AttachmentService(\n store,\n reservations,\n uploadFactory,\n this.backend,\n );\n\n let sweepTimer: ReturnType<typeof setInterval> | undefined;\n if (this.reservationSweepMs !== undefined) {\n const intervalMs = this.reservationSweepMs;\n sweepTimer = setInterval(() => {\n // Sweep failures are retried on the next interval; swallow to prevent\n // unhandled rejection from terminating the process.\n reservations.deleteExpired().catch(() => {});\n }, intervalMs);\n if (typeof sweepTimer.unref === \"function\") {\n sweepTimer.unref();\n }\n }\n\n const destroy = (): void => {\n if (sweepTimer !== undefined) {\n clearInterval(sweepTimer);\n sweepTimer = undefined;\n }\n };\n\n return {\n service,\n store,\n reservations,\n uploadFactory,\n ...(this.backend ? { backend: this.backend } : {}),\n destroy,\n };\n }\n}\n","import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type { Kysely } from \"kysely\";\nimport { parseRef } from \"../../ref.js\";\nimport type { AttachmentReferenceDatabase } from \"./storage/types.js\";\nimport type {\n AttachmentReferenceInput,\n AttachmentReferencePageResult,\n IAttachmentReferenceReader,\n IAttachmentReferenceScanner,\n IAttachmentReferenceWriter,\n} from \"./types.js\";\n\n/** Keyset cursor: the `(document_id, attachment_ref)` pair the last page ended on. */\ntype ScanCursor = {\n documentId: string;\n ref: string;\n};\n\nfunction encodeCursor(cursor: ScanCursor): string {\n return JSON.stringify([cursor.documentId, cursor.ref]);\n}\n\nfunction decodeCursor(cursor: string): ScanCursor {\n let parsed: unknown;\n try {\n parsed = JSON.parse(cursor);\n } catch {\n // Malformed JSON is a bad cursor, not an uncaught SyntaxError leaking out\n // of a scan: report it as the named error the caller expects.\n throw new Error(\n `Invalid attachment reference scan cursor: ${JSON.stringify(cursor)}`,\n );\n }\n if (\n !Array.isArray(parsed) ||\n parsed.length !== 2 ||\n typeof parsed[0] !== \"string\" ||\n parsed[0] === \"\" ||\n typeof parsed[1] !== \"string\" ||\n parsed[1] === \"\"\n ) {\n throw new Error(\n `Invalid attachment reference scan cursor: ${JSON.stringify(cursor)}`,\n );\n }\n return { documentId: parsed[0], ref: parsed[1] };\n}\n\nexport class KyselyAttachmentReferenceStore\n implements\n IAttachmentReferenceReader,\n IAttachmentReferenceScanner,\n IAttachmentReferenceWriter\n{\n constructor(private readonly db: Kysely<AttachmentReferenceDatabase>) {}\n\n async hasReference(documentId: string, ref: AttachmentRef): Promise<boolean> {\n const row = await this.db\n .selectFrom(\"attachment_reference\")\n .select(\"document_id\")\n .where(\"document_id\", \"=\", documentId)\n .where(\"attachment_ref\", \"=\", ref)\n .executeTakeFirst();\n\n return row !== undefined;\n }\n\n async referencingScopes(\n documentId: string,\n ref: AttachmentRef,\n ): Promise<string[]> {\n const rows = await this.db\n .selectFrom(\"attachment_reference\")\n .select(\"scope\")\n .distinct()\n .where(\"document_id\", \"=\", documentId)\n .where(\"attachment_ref\", \"=\", ref)\n .execute();\n\n return rows.map((row) => row.scope);\n }\n\n /**\n * Keyset paging over the unique `(document_id, attachment_ref)` constraint,\n * rather than OFFSET: a scan that runs while the read model is still\n * indexing must not skip or repeat a row because rows appeared behind the\n * cursor. That is exactly the boot case the replicator's re-scan runs in.\n */\n async listReferences(\n cursor: string | undefined,\n limit: number,\n ): Promise<AttachmentReferencePageResult> {\n let query = this.db\n .selectFrom(\"attachment_reference\")\n .select([\"document_id\", \"attachment_ref\"])\n .orderBy(\"document_id\", \"asc\")\n .orderBy(\"attachment_ref\", \"asc\")\n .limit(limit);\n\n if (cursor !== undefined) {\n const after = decodeCursor(cursor);\n query = query.where((eb) =>\n eb.or([\n eb(\"document_id\", \">\", after.documentId),\n eb.and([\n eb(\"document_id\", \"=\", after.documentId),\n eb(\"attachment_ref\", \">\", after.ref),\n ]),\n ]),\n );\n }\n\n const rows = await query.execute();\n const last = rows.at(-1);\n return {\n references: rows.map((row) => ({\n documentId: row.document_id,\n ref: row.attachment_ref as AttachmentRef,\n })),\n nextCursor:\n rows.length === limit && last\n ? encodeCursor({\n documentId: last.document_id,\n ref: last.attachment_ref,\n })\n : undefined,\n };\n }\n\n async addReferences(\n references: readonly AttachmentReferenceInput[],\n ): Promise<void> {\n if (references.length === 0) {\n return;\n }\n\n await this.db\n .insertInto(\"attachment_reference\")\n .values(\n references.map((reference) => ({\n document_id: reference.documentId,\n attachment_ref: reference.ref,\n attachment_hash: parseRef(reference.ref).hash,\n first_operation_id: reference.operationId,\n branch: reference.branch,\n scope: reference.scope,\n first_seen_ordinal: reference.ordinal,\n created_at_utc: new Date().toISOString(),\n })),\n )\n .onConflict((oc) =>\n oc.columns([\"document_id\", \"attachment_ref\"]).doNothing(),\n )\n .execute();\n }\n\n async removeDocuments(documentIds: readonly string[]): Promise<void> {\n if (documentIds.length === 0) {\n return;\n }\n\n await this.db\n .deleteFrom(\"attachment_reference\")\n .where(\"document_id\", \"in\", [...documentIds])\n .execute();\n }\n}\n","import type { Kysely } from \"kysely\";\n\nexport async function up(db: Kysely<any>): Promise<void> {\n await db.schema\n .createTable(\"attachment_reference\")\n .addColumn(\"document_id\", \"text\", (col) => col.notNull())\n .addColumn(\"attachment_ref\", \"text\", (col) => col.notNull())\n .addColumn(\"attachment_hash\", \"text\", (col) => col.notNull())\n .addColumn(\"first_operation_id\", \"text\", (col) => col.notNull())\n .addColumn(\"branch\", \"text\", (col) => col.notNull())\n .addColumn(\"scope\", \"text\", (col) => col.notNull())\n .addColumn(\"first_seen_ordinal\", \"integer\", (col) => col.notNull())\n .addColumn(\"created_at_utc\", \"text\", (col) => col.notNull())\n .addUniqueConstraint(\"unique_attachment_reference_document_ref\", [\n \"document_id\",\n \"attachment_ref\",\n ])\n .execute();\n\n await db.schema\n .createIndex(\"idx_attachment_reference_ref\")\n .on(\"attachment_reference\")\n .column(\"attachment_ref\")\n .execute();\n\n await db.schema\n .createIndex(\"idx_attachment_reference_hash\")\n .on(\"attachment_reference\")\n .column(\"attachment_hash\")\n .execute();\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n await db.schema.dropTable(\"attachment_reference\").ifExists().execute();\n}\n","import { Migrator, sql } from \"kysely\";\nimport type { Kysely, MigrationProvider } from \"kysely\";\nimport * as migration001 from \"./001_create_attachment_reference_table.js\";\n\nexport const ATTACHMENT_REFERENCE_SCHEMA = \"attachment_reference_read_model\";\nexport const ATTACHMENT_REFERENCE_MIGRATION_TABLE =\n \"kysely_migration_attachment_reference_read_model\";\nexport const ATTACHMENT_REFERENCE_MIGRATION_LOCK_TABLE =\n \"kysely_migration_attachment_reference_read_model_lock\";\n\nexport interface AttachmentReferenceMigrationResult {\n success: boolean;\n migrationsExecuted: string[];\n error?: Error;\n}\n\nconst migrations = {\n \"001_create_attachment_reference_table\": migration001,\n};\n\nclass ProgrammaticMigrationProvider implements MigrationProvider {\n getMigrations() {\n return Promise.resolve(migrations);\n }\n}\n\nfunction createMigrator(db: Kysely<unknown>, schema: string): Migrator {\n return new Migrator({\n db: db.withSchema(schema),\n provider: new ProgrammaticMigrationProvider(),\n migrationTableSchema: schema,\n migrationTableName: ATTACHMENT_REFERENCE_MIGRATION_TABLE,\n migrationLockTableName: ATTACHMENT_REFERENCE_MIGRATION_LOCK_TABLE,\n });\n}\n\nfunction toResult(\n error: unknown,\n results:\n | Awaited<ReturnType<Migrator[\"migrateToLatest\"]>>[\"results\"]\n | undefined,\n): AttachmentReferenceMigrationResult {\n const migrationsExecuted =\n results?.map((result) => result.migrationName) ?? [];\n if (error) {\n return {\n success: false,\n migrationsExecuted,\n error:\n error instanceof Error ? error : new Error(\"Unknown migration error\"),\n };\n }\n return { success: true, migrationsExecuted };\n}\n\nexport async function runAttachmentReferenceMigrations(\n db: Kysely<unknown>,\n schema: string = ATTACHMENT_REFERENCE_SCHEMA,\n): Promise<AttachmentReferenceMigrationResult> {\n try {\n await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);\n } catch (error) {\n return {\n success: false,\n migrationsExecuted: [],\n error:\n error instanceof Error ? error : new Error(\"Failed to create schema\"),\n };\n }\n\n try {\n const { error, results } = await createMigrator(\n db,\n schema,\n ).migrateToLatest();\n return toResult(error, results);\n } catch (error) {\n return toResult(error, []);\n }\n}\n\nexport async function rollbackAttachmentReferenceMigration(\n db: Kysely<unknown>,\n schema: string = ATTACHMENT_REFERENCE_SCHEMA,\n): Promise<AttachmentReferenceMigrationResult> {\n try {\n const { error, results } = await createMigrator(db, schema).migrateDown();\n return toResult(error, results);\n } catch (error) {\n return toResult(error, []);\n }\n}\n\nexport async function getAttachmentReferenceMigrationStatus(\n db: Kysely<unknown>,\n schema: string = ATTACHMENT_REFERENCE_SCHEMA,\n) {\n return await createMigrator(db, schema).getMigrations();\n}\n","import type { Kysely } from \"kysely\";\nimport { KyselyAttachmentReferenceStore } from \"./kysely-attachment-reference-store.js\";\nimport type { AttachmentReferenceDatabase } from \"./storage/types.js\";\nimport {\n ATTACHMENT_REFERENCE_SCHEMA,\n runAttachmentReferenceMigrations,\n} from \"./storage/migrations/migrator.js\";\nimport type {\n IAttachmentReferenceReader,\n IAttachmentReferenceWriter,\n} from \"./types.js\";\n\nexport type AttachmentReferenceIndexBuildResult = {\n store: IAttachmentReferenceReader & IAttachmentReferenceWriter;\n};\n\nexport class AttachmentReferenceIndexBuilder {\n constructor(private readonly db: Kysely<unknown>) {}\n\n async build(): Promise<AttachmentReferenceIndexBuildResult> {\n const result = await runAttachmentReferenceMigrations(this.db);\n if (!result.success && result.error) {\n throw result.error;\n }\n\n const scopedDb = this.db.withSchema(\n ATTACHMENT_REFERENCE_SCHEMA,\n ) as Kysely<AttachmentReferenceDatabase>;\n const store = new KyselyAttachmentReferenceStore(scopedDb);\n return { store };\n }\n}\n","import {\n BaseReadModel,\n defaultReadModelIndexingConfig,\n type DocumentViewDatabase,\n type IConsistencyTracker,\n type IDocumentModelRegistry,\n type IOperationIndex,\n type IWriteCache,\n} from \"@powerhousedao/reactor\";\nimport {\n isPurgeMarker,\n type OperationWithContext,\n} from \"@powerhousedao/shared/document-model\";\nimport type { Kysely } from \"kysely\";\nimport type { IAttachmentSchemaCompiler } from \"../../reference-index/types.js\";\nimport type {\n AttachmentReferenceInput,\n IAttachmentReferenceWriter,\n} from \"./types.js\";\n\nexport const ATTACHMENT_REFERENCE_READ_MODEL_ID =\n \"attachment-reference-read-model\";\n\n/**\n * Indexes attachment references in chunks.\n *\n * Each reference row stands on its own and is written insert-or-do-nothing, so\n * a batch that commits in pieces exposes no partial structure, and re-indexing\n * the same range writes nothing new: the catch-up sweep needs no stream suffix.\n *\n * The rows live on another handle, so the purge fence is \"skip\": a tombstoned\n * id's operations are dropped at commit and its marker deletes its rows.\n */\nexport class AttachmentReferenceReadModel extends BaseReadModel {\n constructor(\n db: Kysely<DocumentViewDatabase>,\n operationIndex: IOperationIndex,\n writeCache: IWriteCache,\n consistencyTracker: IConsistencyTracker,\n private readonly documentModelRegistry: IDocumentModelRegistry,\n private readonly schemaCompiler: IAttachmentSchemaCompiler,\n private readonly referenceWriter: IAttachmentReferenceWriter,\n ) {\n super(db, operationIndex, writeCache, consistencyTracker, {\n readModelId: ATTACHMENT_REFERENCE_READ_MODEL_ID,\n rebuildStateOnInit: false,\n indexing: defaultReadModelIndexingConfig,\n replayStreamSuffix: false,\n purgeFence: \"skip\",\n });\n }\n\n protected override async commitOperations(\n items: OperationWithContext[],\n ): Promise<void> {\n const references: AttachmentReferenceInput[] = [];\n const purged = new Set<string>();\n\n for (const { operation, context } of items) {\n if (isPurgeMarker(operation)) {\n purged.add(context.documentId);\n continue;\n }\n if (operation.error !== undefined) continue;\n\n const module = this.documentModelRegistry.getModule(context.documentType);\n const extractor = this.schemaCompiler.forModuleAction(\n module,\n operation.action.type,\n );\n const refs = extractor.extract(operation.action);\n\n for (const ref of refs) {\n references.push({\n documentId: context.documentId,\n ref,\n operationId: operation.id,\n branch: context.branch,\n scope: context.scope,\n ordinal: context.ordinal,\n });\n }\n }\n\n if (references.length > 0) {\n await this.referenceWriter.addReferences(references);\n }\n if (purged.size > 0) {\n await this.referenceWriter.removeDocuments([...purged]);\n }\n }\n}\n","export const DEFAULT_S3_ATTACHMENT_PREFIX = \"attachments\";\nexport const DEFAULT_S3_UPLOAD_TTL_SECONDS = 900;\nexport const DEFAULT_S3_DOWNLOAD_TTL_SECONDS = 300;\nexport const MAX_S3_PRESIGN_TTL_SECONDS = 604_800;\n\nexport type S3AttachmentConfig = {\n endpoint: string;\n region: string;\n bucket: string;\n accessKeyId: string;\n secretAccessKey: string;\n prefix: string;\n forcePathStyle: boolean;\n uploadTtlSeconds: number;\n downloadTtlSeconds: number;\n};\n\nexport type AttachmentStorageConfig =\n | { kind: \"filesystem\" }\n | { kind: \"s3\"; s3: S3AttachmentConfig };\n\ntype Environment = Readonly<Record<string, string | undefined>>;\n\nfunction required(env: Environment, name: string): string {\n const value = env[name];\n if (value === undefined || value.trim().length === 0) {\n throw new Error(`${name} is required and must not be blank`);\n }\n if (value.trim() !== value) {\n throw new Error(`${name} must not have leading or trailing whitespace`);\n }\n return value;\n}\n\nconst LOOPBACK_HOSTNAMES = new Set([\"127.0.0.1\", \"localhost\", \"[::1]\"]);\n\n/** Plain HTTP is allowed only for loopback hosts (local S3 emulators). */\nfunction isLoopbackHost(endpoint: URL): boolean {\n return LOOPBACK_HOSTNAMES.has(endpoint.hostname.toLowerCase());\n}\n\nfunction parseEndpoint(value: string): string {\n const message =\n \"PH_ATTACHMENT_S3_ENDPOINT must be a valid HTTPS URL (HTTP is allowed only for loopback hosts)\";\n let endpoint: URL;\n try {\n endpoint = new URL(value);\n } catch {\n throw new Error(message);\n }\n const protocolAllowed =\n endpoint.protocol === \"https:\" ||\n (endpoint.protocol === \"http:\" && isLoopbackHost(endpoint));\n if (\n value.trim() !== value ||\n !protocolAllowed ||\n endpoint.username !== \"\" ||\n endpoint.password !== \"\" ||\n endpoint.search !== \"\" ||\n endpoint.hash !== \"\"\n ) {\n throw new Error(message);\n }\n return endpoint.toString().replace(/\\/$/, \"\");\n}\n\nfunction parseBoolean(\n env: Environment,\n name: string,\n defaultValue: boolean,\n): boolean {\n const value = env[name];\n if (value === undefined) return defaultValue;\n if (value === \"true\") return true;\n if (value === \"false\") return false;\n throw new Error(`${name} must be either true or false`);\n}\n\nfunction parseTtl(\n env: Environment,\n name: string,\n defaultValue: number,\n): number {\n const value = env[name];\n if (value === undefined) return defaultValue;\n if (!/^[1-9]\\d*$/.test(value)) {\n throw new Error(`${name} must be a positive integer`);\n }\n const seconds = Number(value);\n if (!Number.isSafeInteger(seconds) || seconds > MAX_S3_PRESIGN_TTL_SECONDS) {\n throw new Error(\n `${name} must be between 1 and ${MAX_S3_PRESIGN_TTL_SECONDS}`,\n );\n }\n return seconds;\n}\n\nexport function normalizeS3AttachmentPrefix(prefix: string): string {\n if (\n prefix.trim() !== prefix ||\n prefix.startsWith(\"/\") ||\n prefix.includes(\"\\\\\")\n ) {\n throw new Error(\"S3_ATTACHMENT_PREFIX is unsafe\");\n }\n const normalized = prefix.replace(/\\/+$/, \"\");\n if (normalized.length === 0) {\n throw new Error(\"S3_ATTACHMENT_PREFIX must not be blank\");\n }\n const segments = normalized.split(\"/\");\n if (\n segments.some(\n (segment) =>\n segment.length === 0 ||\n segment === \".\" ||\n segment === \"..\" ||\n !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment),\n )\n ) {\n throw new Error(\"S3_ATTACHMENT_PREFIX contains an unsafe segment\");\n }\n return segments.join(\"/\");\n}\n\nexport function parseAttachmentStorageConfig(\n env: Environment = process.env,\n): AttachmentStorageConfig {\n const selector = env.PH_ATTACHMENT_STORAGE;\n if (selector === undefined || selector === \"filesystem\") {\n return { kind: \"filesystem\" };\n }\n if (selector !== \"s3\") {\n throw new Error(\"PH_ATTACHMENT_STORAGE must be either filesystem or s3\");\n }\n\n return {\n kind: \"s3\",\n s3: {\n endpoint: parseEndpoint(required(env, \"PH_ATTACHMENT_S3_ENDPOINT\")),\n region: required(env, \"PH_ATTACHMENT_S3_REGION\"),\n bucket: required(env, \"PH_ATTACHMENT_S3_BUCKET\"),\n accessKeyId: required(env, \"PH_ATTACHMENT_S3_ACCESS_KEY_ID\"),\n secretAccessKey: required(env, \"PH_ATTACHMENT_S3_SECRET_ACCESS_KEY\"),\n prefix: normalizeS3AttachmentPrefix(\n env.S3_ATTACHMENT_PREFIX ?? DEFAULT_S3_ATTACHMENT_PREFIX,\n ),\n forcePathStyle: parseBoolean(\n env,\n \"PH_ATTACHMENT_S3_FORCE_PATH_STYLE\",\n false,\n ),\n uploadTtlSeconds: parseTtl(\n env,\n \"PH_ATTACHMENT_S3_UPLOAD_TTL_SECONDS\",\n DEFAULT_S3_UPLOAD_TTL_SECONDS,\n ),\n downloadTtlSeconds: parseTtl(\n env,\n \"PH_ATTACHMENT_S3_DOWNLOAD_TTL_SECONDS\",\n DEFAULT_S3_DOWNLOAD_TTL_SECONDS,\n ),\n },\n };\n}\n","import { Buffer } from \"node:buffer\";\n\nconst SHA256_HEX = /^[0-9a-f]{64}$/;\n\nfunction validateHash(hash: string): void {\n if (!SHA256_HEX.test(hash)) {\n throw new Error(\n \"Attachment hash must be 64 lowercase hexadecimal characters\",\n );\n }\n}\n\nexport function deriveS3AttachmentKey(hash: string, prefix: string): string {\n validateHash(hash);\n // Imported lazily at the call boundary to keep this primitive's validation\n // identical to configuration parsing without accepting unnormalized input.\n if (\n prefix.startsWith(\"/\") ||\n prefix.endsWith(\"/\") ||\n prefix.includes(\"\\\\\") ||\n prefix\n .split(\"/\")\n .some(\n (segment) =>\n segment.length === 0 ||\n segment === \".\" ||\n segment === \"..\" ||\n !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment),\n )\n ) {\n throw new Error(\"S3 attachment prefix must be normalized and safe\");\n }\n return `${prefix}/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash}`;\n}\n\nexport function sha256HexToBase64(hash: string): string {\n validateHash(hash);\n return Buffer.from(hash, \"hex\").toString(\"base64\");\n}\n","import {\n GetObjectCommand,\n HeadObjectCommand,\n PutObjectCommand,\n S3Client,\n} from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type {\n AttachmentDownloadTarget,\n AttachmentUploadTarget,\n} from \"../../types.js\";\nimport type { S3AttachmentConfig } from \"./config.js\";\nimport { deriveS3AttachmentKey, sha256HexToBase64 } from \"./keying.js\";\n\nexport interface S3CommandClient {\n send(command: object): Promise<unknown>;\n}\n\nexport type S3Presigner = (\n client: object,\n command: object,\n expiresInSeconds: number,\n) => Promise<string>;\n\ntype Dependencies = {\n client?: S3CommandClient;\n presign?: S3Presigner;\n now?: () => Date;\n};\n\nconst defaultPresigner: S3Presigner = (client, command, expiresInSeconds) =>\n getSignedUrl(client as S3Client, command as never, {\n expiresIn: expiresInSeconds,\n unhoistableHeaders: new Set([\"x-amz-checksum-sha256\"]),\n });\n\nexport class S3AttachmentPrimitives {\n readonly client: S3CommandClient;\n readonly presign: S3Presigner;\n readonly now: () => Date;\n\n constructor(\n readonly config: S3AttachmentConfig,\n dependencies: Dependencies = {},\n ) {\n this.client =\n dependencies.client ??\n new S3Client({\n endpoint: config.endpoint,\n region: config.region,\n forcePathStyle: config.forcePathStyle,\n credentials: {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n },\n });\n this.presign = dependencies.presign ?? defaultPresigner;\n this.now = dependencies.now ?? (() => new Date());\n }\n\n buildHeadObjectCommand(hash: string): object {\n return new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: deriveS3AttachmentKey(hash, this.config.prefix),\n });\n }\n\n buildPutObjectCommand(hash: string, mimeType: string): object {\n if (\n mimeType.trim().length === 0 ||\n mimeType.includes(\"\\r\") ||\n mimeType.includes(\"\\n\")\n ) {\n throw new Error(\n \"Attachment MIME type must not be blank or contain newlines\",\n );\n }\n return new PutObjectCommand({\n Bucket: this.config.bucket,\n Key: deriveS3AttachmentKey(hash, this.config.prefix),\n ContentType: mimeType,\n ChecksumSHA256: sha256HexToBase64(hash),\n });\n }\n\n buildGetObjectCommand(hash: string): object {\n return new GetObjectCommand({\n Bucket: this.config.bucket,\n Key: deriveS3AttachmentKey(hash, this.config.prefix),\n });\n }\n\n headObject(hash: string): Promise<unknown> {\n return this.client.send(this.buildHeadObjectCommand(hash));\n }\n\n async createUploadTarget(\n hash: string,\n mimeType: string,\n ttlSeconds = this.config.uploadTtlSeconds,\n ): Promise<AttachmentUploadTarget> {\n const checksum = sha256HexToBase64(hash);\n const url = await this.presign(\n this.client,\n this.buildPutObjectCommand(hash, mimeType),\n ttlSeconds,\n );\n return {\n kind: \"presigned-put\",\n method: \"PUT\",\n url,\n headers: {\n \"content-type\": mimeType,\n \"x-amz-checksum-sha256\": checksum,\n },\n expiresAtUtc: new Date(\n this.now().getTime() + ttlSeconds * 1_000,\n ).toISOString(),\n };\n }\n\n async createDownloadTarget(\n hash: string,\n ttlSeconds = this.config.downloadTtlSeconds,\n ): Promise<AttachmentDownloadTarget> {\n const url = await this.presign(\n this.client,\n this.buildGetObjectCommand(hash),\n ttlSeconds,\n );\n return {\n kind: \"presigned-get\",\n method: \"GET\",\n url,\n headers: {},\n expiresAtUtc: new Date(\n this.now().getTime() + ttlSeconds * 1_000,\n ).toISOString(),\n };\n }\n}\n\nexport function createS3AttachmentPrimitives(\n config: S3AttachmentConfig,\n dependencies: Dependencies = {},\n): S3AttachmentPrimitives {\n return new S3AttachmentPrimitives(config, dependencies);\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { Kysely } from \"kysely\";\nimport type { IAttachmentBackend } from \"../../interfaces.js\";\nimport type {\n AttachmentBackendHealth,\n AttachmentDownloadTarget,\n AttachmentUploadTarget,\n Reservation,\n} from \"../../types.js\";\nimport type { AttachmentDatabase } from \"../kysely/types.js\";\nimport type { S3AttachmentConfig } from \"./config.js\";\nimport { deriveS3AttachmentKey } from \"./keying.js\";\nimport {\n S3AttachmentPrimitives,\n type S3CommandClient,\n type S3Presigner,\n} from \"./primitives.js\";\n\nconst READINESS_PROBE_HASH = \"0\".repeat(64);\nconst OBJECT_NOT_FOUND_NAMES = new Set([\n \"NotFound\",\n \"NoSuchKey\",\n \"NoSuchObject\",\n]);\n\ntype ProviderError = {\n name?: unknown;\n code?: unknown;\n $metadata?: { httpStatusCode?: unknown };\n};\n\nexport type S3AttachmentBackendDependencies = {\n client?: S3CommandClient;\n presign?: S3Presigner;\n now?: () => Date;\n uploadTtlSeconds?: number;\n downloadTtlSeconds?: number;\n};\n\nfunction isObjectNotFound(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null) return false;\n const providerError = error as ProviderError;\n if (providerError.$metadata?.httpStatusCode !== 404) return false;\n return [providerError.name, providerError.code].some(\n (code) => typeof code === \"string\" && OBJECT_NOT_FOUND_NAMES.has(code),\n );\n}\n\nfunction requireHashFirstReservation(\n reservation: Reservation,\n): asserts reservation is Reservation & {\n clientHash: AttachmentHash;\n sizeBytes: number;\n} {\n if (reservation.clientHash === null) {\n throw new Error(\"S3 attachment reservations require a client hash\");\n }\n deriveS3AttachmentKey(reservation.clientHash, \"validation\");\n if (\n reservation.sizeBytes === null ||\n !Number.isSafeInteger(reservation.sizeBytes) ||\n reservation.sizeBytes <= 0\n ) {\n throw new Error(\n \"S3 attachment reservation sizeBytes must be a positive safe integer\",\n );\n }\n}\n\n/** Server-only S3 capability. Callers complete authorization before download. */\nexport class S3AttachmentBackend implements IAttachmentBackend {\n readonly kind = \"s3\" as const;\n private readonly primitives: S3AttachmentPrimitives;\n private readonly now: () => Date;\n private readonly uploadTtlSeconds: number;\n private readonly downloadTtlSeconds: number;\n\n constructor(\n private readonly db: Kysely<AttachmentDatabase>,\n readonly config: S3AttachmentConfig,\n dependencies: S3AttachmentBackendDependencies = {},\n ) {\n this.primitives = new S3AttachmentPrimitives(config, dependencies);\n this.now = dependencies.now ?? (() => new Date());\n this.uploadTtlSeconds =\n dependencies.uploadTtlSeconds ?? config.uploadTtlSeconds;\n this.downloadTtlSeconds =\n dependencies.downloadTtlSeconds ?? config.downloadTtlSeconds;\n }\n\n async prepareUploadTarget(\n reservation: Reservation,\n ): Promise<AttachmentUploadTarget> {\n requireHashFirstReservation(reservation);\n const hash = reservation.clientHash;\n const now = this.now().toISOString();\n const storagePath = deriveS3AttachmentKey(hash, this.config.prefix);\n\n try {\n await this.db\n .insertInto(\"attachment\")\n .values({\n hash,\n mime_type: reservation.mimeType,\n file_name: reservation.fileName,\n size_bytes: reservation.sizeBytes,\n extension: reservation.extension,\n status: \"available\",\n storage_path: storagePath,\n source: \"local\",\n created_at_utc: reservation.createdAtUtc,\n last_accessed_at_utc: now,\n })\n .onConflict((conflict) =>\n conflict.column(\"hash\").doUpdateSet({\n mime_type: reservation.mimeType,\n file_name: reservation.fileName,\n size_bytes: reservation.sizeBytes,\n extension: reservation.extension,\n status: \"available\",\n storage_path: storagePath,\n last_accessed_at_utc: now,\n }),\n )\n .execute();\n } catch {\n throw new Error(\"S3 attachment metadata registration failed\");\n }\n\n let target: AttachmentUploadTarget;\n try {\n target = await this.primitives.createUploadTarget(\n hash,\n reservation.mimeType,\n this.uploadTtlSeconds,\n );\n } catch {\n throw new Error(\"S3 attachment upload target preparation failed\");\n }\n\n // Direct S3 completion is invisible to Switchboard; the existing sweep\n // expires this reservation normally.\n return target;\n }\n\n async prepareDownloadTarget(\n hash: AttachmentHash,\n ttlSeconds?: number,\n ): Promise<AttachmentDownloadTarget> {\n try {\n return await this.primitives.createDownloadTarget(\n hash,\n ttlSeconds ?? this.downloadTtlSeconds,\n );\n } catch {\n throw new Error(\"S3 attachment download target preparation failed\");\n }\n }\n\n async exists(hash: AttachmentHash): Promise<boolean> {\n const metadata = await this.db\n .selectFrom(\"attachment\")\n .select(\"hash\")\n .where(\"hash\", \"=\", hash)\n .executeTakeFirst();\n if (!metadata) return false;\n\n try {\n await this.primitives.headObject(hash);\n return true;\n } catch (error) {\n if (isObjectNotFound(error)) return false;\n // Provider errors may contain endpoints or signed request details, so the\n // raw cause intentionally must not cross this backend boundary.\n // eslint-disable-next-line preserve-caught-error\n throw new Error(\"S3 attachment existence check failed\");\n }\n }\n\n async health(): Promise<AttachmentBackendHealth> {\n try {\n await this.primitives.headObject(READINESS_PROBE_HASH);\n return { kind: this.kind, ready: true };\n } catch (error) {\n return {\n kind: this.kind,\n ready: isObjectNotFound(error),\n };\n }\n }\n}\n\nexport function createS3AttachmentBackend(\n db: Kysely<AttachmentDatabase>,\n config: S3AttachmentConfig,\n dependencies: S3AttachmentBackendDependencies = {},\n): S3AttachmentBackend {\n return new S3AttachmentBackend(db, config, dependencies);\n}\n"],"mappings":"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"}
|
|
@@ -340,6 +340,9 @@ interface IAttachmentUpload {
|
|
|
340
340
|
/**
|
|
341
341
|
* Stream attachment data through this handle.
|
|
342
342
|
*
|
|
343
|
+
* `data` may be a Blob: a remote handle sends it as the request body
|
|
344
|
+
* as-is, which a browser streams from disk instead of buffering.
|
|
345
|
+
*
|
|
343
346
|
* The handle manages the full upload lifecycle internally:
|
|
344
347
|
* writing bytes to the backing store, computing or verifying
|
|
345
348
|
* the content hash, creating the attachment record, and
|
|
@@ -371,7 +374,7 @@ interface IAttachmentUpload {
|
|
|
371
374
|
* upload as done. A handle that transfers nothing itself (the S3 handle,
|
|
372
375
|
* which requires its presigned target) has nothing to abort.
|
|
373
376
|
*/
|
|
374
|
-
send(data: ReadableStream<Uint8Array
|
|
377
|
+
send(data: ReadableStream<Uint8Array> | Blob, options?: AttachmentSendOptions): Promise<AttachmentUploadResult>;
|
|
375
378
|
}
|
|
376
379
|
/**
|
|
377
380
|
* Read-only subset of IAttachmentStore.
|
|
@@ -565,4 +568,4 @@ interface IAttachmentBackend {
|
|
|
565
568
|
//#endregion
|
|
566
569
|
export { UploadFirstReserveAttachmentOptions as A, AttachmentUploadResult as C, ReserveAttachmentOptions as D, Reservation as E, TransportFetchResult as O, AttachmentTransportConfig as S, HashFirstReserveAttachmentOptions as T, AttachmentResponse as _, IAttachmentTransport as a, AttachmentStatus as b, IAttachmentUploadFactory as c, AttachmentBackendKind as d, AttachmentDownloadOptions as f, AttachmentMetadata as g, AttachmentHeader as h, IAttachmentStore as i, TransportResponse as k, IReservationStore as l, AttachmentDownloadTargetOptions as m, IAttachmentReader as n, IAttachmentTransportFactory as o, AttachmentDownloadTarget as p, IAttachmentService as r, IAttachmentUpload as s, IAttachmentBackend as t, AttachmentBackendHealth as u, AttachmentSendOptions as v, AttachmentUploadTarget as w, AttachmentTargetHeaders as x, AttachmentStatOptions as y };
|
|
567
570
|
export {};
|
|
568
|
-
//# sourceMappingURL=interfaces-
|
|
571
|
+
//# sourceMappingURL=interfaces-C4GRlKeB.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interfaces-
|
|
1
|
+
{"version":3,"file":"interfaces-C4GRlKeB.d.ts","names":[],"sources":["../src/types.ts","../src/interfaces.ts"],"mappings":";;KAEY;KAEA;EACV,MAAM;EACN;;;;;;;KAQU;;;;;;;KAQA;EACV,MAAM;EACN;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;;;;;;;;;;;;;;;;;;;;;KAsBU;EACV;EACA;EACA;EACA;EACA;EACA;;;;;;KAOU;EACV;EACA;EACA;EACA;EACA;;;;;;;;;KAUU;EACV;EACA;EACA;;;;EAIA,YAAY;;;;;;EAMZ;;;;;;;;;;;;KAaU,2BACR,sCACA;;;;KAKQ;EACV,MAAM;EACN,KAAK;EACL,QAAQ;;;;;;;;;;KAWE;EACV,cAAc,gBAAgB;EAC9B,SAAS;;;;;;;KAQC;EACV;EACA,SAAS;;;KAIC;EACV;;;;;;;;KASU;EACV;EACA;EACA,SAAS;;;KAIC,0BAA0B,SAAS;KAEnC;EACV;EACA;EACA;EACA,SAAS;EACT;;KAGU;EACV;EACA;EACA;EACA,SAAS;EACT;;;KAIU,yBACR,oCACA;KAEQ;EACV;EACA;EACA;EACA,SAAS;EACT;;KAGU;EACV;EACA;EACA;EACA,SAAS;EACT;;;KAIU,2BACR,sCACA;;;;KAKQ;EACV,QAAQ;EACR,MAAM,eAAe;;;;;;;;KASX;EACV,MAAM;EACN,UAAU;EACV,MAAM,eAAe;;;;;;;;KASX;EACN;EAAc,UAAU;;EAExB;EACA,MAAM;EACN;EACA;;EAEA;;EAEA;;EAEA;;;;;KAKM;EACV;EACA,YAAY;;;;;;;;;KAUF;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;EAMA,eAAe;;;;;;;;UC3PA;;;;;;;;;;;;;;;;;;;EAmBf,QAAQ,SAAS,2BAA2B,QAAQ;;;;;;;;;;;;EAapD,KACE,KAAK,eACL,UAAU,wBACT,QAAQ;;;;;;;;;;;;;;;;;EAkBX,IACE,KAAK,eACL,UAAU,cAAc,4BACvB,QAAQ;;;;;;;;;;;EAYX,kBACE,KAAK,eACL,SAAS,kCACR,QAAQ;;;;;;UAOI;;;;EAIf;;;;;;EAOA,KAAK;;;;;;WAOI;;WAGA,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCxB,KACE,MAAM,eAAe,cAAc,MACnC,UAAU,wBACT,QAAQ;;;;;;;;;;;UAYI;;;;;;;;;;;;;;EAcf,KAAK,MAAM,gBAAgB,sBAAsB,QAAQ;;;;;;;;;;;;;;;;;;;EAoBzD,IACE,MAAM,gBACN,SAAS,aACT,sBACC,QAAQ;;;;;;EAOX,mBACE,MAAM,gBACN,SAAS,kCACR,QAAQ;;;;;;;;UASI,yBAAyB;;;;;;;EAOxC,IAAI,MAAM,iBAAiB;;;;;;;;;;;;EAa3B,IACE,MAAM,gBACN,UAAU,oBACV,MAAM,eAAe,cACpB;;;;;;;;;;;;;;;;EAiBH,MAAM,MAAM,iBAAiB;;;;;EAM7B,eAAe;;;;;;;;;UAUA;;;;;;;;;;;;;;;EAef,MACE,MAAM,gBACN,oBACA,SAAS,cACR,QAAQ;;;;;;;EAQX,SAAS,MAAM,iBAAiB;;;;;;;;;;;EAYhC,KACE,MAAM,gBACN,gBACA,MAAM,eAAe,cACpB;;;;;;UAOY;EACf,SAAS,QAAQ,4BAA4B;;;;;;UAO9B;EACf,OAAO,SAAS,2BAA2B,QAAQ;EACnD,IAAI,wBAAwB,QAAQ;EACpC,OAAO,wBAAwB;;;;;;;;EAS/B,cAAc,MAAM,OAAO;;;;;;;UAQZ;EACf,aAAa,aAAa,cAAc;;;;;;;UAQzB;WACN,MAAM;EACf,oBACE,aAAa,cACZ,QAAQ;;;;;;EAMX,sBACE,MAAM,gBACN,sBACC,QAAQ;EACX,OAAO,MAAM,iBAAiB;EAC9B,UAAU,QAAQ"}
|