@powerhousedao/reactor-attachments 6.2.3-dev.3 → 6.2.3-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +38 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +87 -23
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $ as AttachmentPending, A as IReservationStore, B as AttachmentStatus, C as IAttachmentReader, D as IAttachmentTransportFactory, E as IAttachmentTransport, G as HashFirstReserveAttachmentOptions, H as AttachmentTransportConfig, I as AttachmentHeader, J as TransportFetchResult, K as Reservation, L as AttachmentMetadata, M as AttachmentBackendKind, N as AttachmentDownloadOptions, O as IAttachmentUpload, P as AttachmentDownloadTarget, Q as AttachmentNotFound, R as AttachmentResponse, S as IAttachmentBackend, T as IAttachmentStore, U as AttachmentUploadResult, V as AttachmentTargetHeaders, W as AttachmentUploadTarget, X as UploadFirstReserveAttachmentOptions, Y as TransportResponse, Z as AttachmentAlreadyExists, _ as createRef, a as RemoteAttachmentUpload, at as SizeMismatch, b as parseAttachmentDownloadTarget, c as XhrUploadTransportOptions, d as AttachmentUploadResponse, et as AttachmentTransferError, f as AttachmentUploadTransport, g as ParsedRef, h as SwitchboardTransportConfig, i as RemoteAttachmentUploadFactory, it as ReservationNotFound, j as AttachmentBackendHealth, k as IAttachmentUploadFactory, l as createXhrUploadTransport, m as SwitchboardAttachmentTransport, n as createRemoteAttachmentService, nt as HashMismatch, o as RemoteReservationStore, ot as UploadTooLarge, p as createFetchUploadTransport, q as ReserveAttachmentOptions, r as RemoteAttachmentStore, rt as InvalidAttachmentRef, s as SwitchboardClientConfig, t as NullAttachmentTransport, tt as AttachmentTransferStage, u as AttachmentUploadRequest, v as parseRef, w as IAttachmentService, x as parseAttachmentUploadTarget, y as AttachmentService, z as AttachmentSendOptions } from "./null-attachment-transport-CMrO_ZKA.js";
|
|
2
2
|
import * as kysely from "kysely";
|
|
3
|
-
import { Kysely } from "kysely";
|
|
3
|
+
import { Kysely, Transaction } from "kysely";
|
|
4
4
|
import { AttachmentHash, AttachmentRef, BaseReadModel, DocumentViewDatabase, IConsistencyTracker, IDocumentModelRegistry, IOperationIndex, IWriteCache } from "@powerhousedao/reactor";
|
|
5
5
|
import { Action, DocumentModelModule, OperationWithContext } from "@powerhousedao/shared/document-model";
|
|
6
6
|
|
|
@@ -200,13 +200,49 @@ declare class AttachmentReferenceReadModel extends BaseReadModel {
|
|
|
200
200
|
private readonly schemaCompiler;
|
|
201
201
|
private readonly referenceWriter;
|
|
202
202
|
private indexingQueue;
|
|
203
|
+
private checkpointTarget;
|
|
204
|
+
/**
|
|
205
|
+
* How far a replay may skip ahead of the cursor: the highest ordinal already
|
|
206
|
+
* pulled from the index while the cursor was parked.
|
|
207
|
+
*
|
|
208
|
+
* NOT a claim that every ordinal below it committed -- the parked ordinal is
|
|
209
|
+
* precisely the one that did not -- so it is a hint, and {@link replayFrom}
|
|
210
|
+
* re-probes the gap before honouring it.
|
|
211
|
+
*/
|
|
212
|
+
private replayedThrough;
|
|
213
|
+
private warnedCheckpoint;
|
|
203
214
|
constructor(db: Kysely<DocumentViewDatabase>, operationIndex: IOperationIndex, writeCache: IWriteCache, consistencyTracker: IConsistencyTracker, documentModelRegistry: IDocumentModelRegistry, schemaCompiler: IAttachmentSchemaCompiler, referenceWriter: IAttachmentReferenceWriter);
|
|
204
215
|
indexOperations(items: OperationWithContext[]): Promise<void>;
|
|
205
216
|
init(): Promise<void>;
|
|
206
217
|
private enqueue;
|
|
207
218
|
protected commitOperations(items: OperationWithContext[]): Promise<void>;
|
|
208
219
|
private indexOperationsInOrdinalOrder;
|
|
209
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Writes the cursor parked by indexOperationsInOrdinalOrder instead of the
|
|
222
|
+
* batch maximum, so indexing an operation above a gap never advances past it.
|
|
223
|
+
*/
|
|
224
|
+
protected saveState(trx: Transaction<DocumentViewDatabase>, items: OperationWithContext[]): Promise<void>;
|
|
225
|
+
/** Last ordinal of the contiguous run starting at lastOrdinal + 1. */
|
|
226
|
+
private contiguousEnd;
|
|
227
|
+
/**
|
|
228
|
+
* Opens a replay at the lowest ordinal still worth reading.
|
|
229
|
+
*
|
|
230
|
+
* {@link replayedThrough} is what keeps a permanently held hole -- a
|
|
231
|
+
* rolled-back insert, which never fills -- from re-reading the whole tail on
|
|
232
|
+
* every batch. It cannot be trusted on its own: a hole that fills without
|
|
233
|
+
* being delivered here is visible only in the index, and a mark that is
|
|
234
|
+
* never questioned would hide that operation for the life of the process.
|
|
235
|
+
* That happens whenever another writer commits the gap (a second reactor on
|
|
236
|
+
* the same database), and it leaves the cursor parked below an operation
|
|
237
|
+
* whose references were never written -- `hasReference` then answers false
|
|
238
|
+
* for an attachment that is genuinely referenced.
|
|
239
|
+
*
|
|
240
|
+
* So a parked batch spends one page probing the gap: the first row above the
|
|
241
|
+
* cursor is the missing ordinal itself once it commits. Finding it drops the
|
|
242
|
+
* mark and replays from the cursor; not finding it leaves the mark standing,
|
|
243
|
+
* which is the cheap path and the common one.
|
|
244
|
+
*/
|
|
245
|
+
private replayFrom;
|
|
210
246
|
private loadThroughOrdinal;
|
|
211
247
|
private sortAndDedupe;
|
|
212
248
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/storage/kysely/types.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.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/attachment-builder.ts","../src/reference-index/types.ts","../src/reference-index/attachment-schema-compiler.ts","../src/read-models/attachment-reference/types.ts","../src/read-models/attachment-reference/index-builder.ts","../src/read-models/attachment-reference/attachment-reference-read-model.ts","../src/read-models/attachment-reference/storage/types.ts","../src/read-models/attachment-reference/kysely-attachment-reference-store.ts","../src/read-models/attachment-reference/storage/migrations/migrator.ts","../src/storage/s3/config.ts","../src/storage/s3/keying.ts","../src/storage/s3/primitives.ts","../src/storage/s3/backend.ts","../src/storage/s3/upload-factory.ts"],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/storage/kysely/types.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.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/attachment-builder.ts","../src/reference-index/types.ts","../src/reference-index/attachment-schema-compiler.ts","../src/read-models/attachment-reference/types.ts","../src/read-models/attachment-reference/index-builder.ts","../src/read-models/attachment-reference/attachment-reference-read-model.ts","../src/read-models/attachment-reference/storage/types.ts","../src/read-models/attachment-reference/kysely-attachment-reference-store.ts","../src/read-models/attachment-reference/storage/migrations/migrator.ts","../src/storage/s3/config.ts","../src/storage/s3/keying.ts","../src/storage/s3/primitives.ts","../src/storage/s3/backend.ts","../src/storage/s3/upload-factory.ts"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { mkdir, rename, rm } from "node:fs/promises";
|
|
|
5
5
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
|
+
import { finished } from "node:stream/promises";
|
|
8
9
|
import { generatorTypeDefs } from "@powerhousedao/document-engineering/graphql";
|
|
9
10
|
import { constantCase, pascalCase } from "change-case";
|
|
10
11
|
import { Kind, buildASTSchema, getNamedType, isInputObjectType, isListType, isNonNullType, parse, print } from "graphql";
|
|
@@ -46,6 +47,13 @@ function storageRelativePath(hash) {
|
|
|
46
47
|
*
|
|
47
48
|
* Returns the number of bytes written.
|
|
48
49
|
*/
|
|
50
|
+
async function discardTempFile(writer, tempPath) {
|
|
51
|
+
writer.destroy();
|
|
52
|
+
try {
|
|
53
|
+
await finished(writer, { error: false });
|
|
54
|
+
} catch {}
|
|
55
|
+
await rm(tempPath, { force: true });
|
|
56
|
+
}
|
|
49
57
|
async function writeAttachmentBytes(path, data) {
|
|
50
58
|
await mkdir(dirname(path), { recursive: true });
|
|
51
59
|
const tempPath = join(dirname(path), `${randomUUID()}.tmp`);
|
|
@@ -82,8 +90,7 @@ async function writeAttachmentBytes(path, data) {
|
|
|
82
90
|
reader.releaseLock();
|
|
83
91
|
}
|
|
84
92
|
if (caughtError) {
|
|
85
|
-
writer
|
|
86
|
-
await rm(tempPath, { force: true });
|
|
93
|
+
await discardTempFile(writer, tempPath);
|
|
87
94
|
throw caughtError;
|
|
88
95
|
}
|
|
89
96
|
try {
|
|
@@ -96,7 +103,7 @@ async function writeAttachmentBytes(path, data) {
|
|
|
96
103
|
});
|
|
97
104
|
await rename(tempPath, path);
|
|
98
105
|
} catch (err) {
|
|
99
|
-
await
|
|
106
|
+
await discardTempFile(writer, tempPath);
|
|
100
107
|
throw err instanceof Error ? err : new Error(String(err));
|
|
101
108
|
}
|
|
102
109
|
return bytesWritten;
|
|
@@ -1167,6 +1174,17 @@ var AttachmentReferenceIndexBuilder = class {
|
|
|
1167
1174
|
const ATTACHMENT_REFERENCE_READ_MODEL_ID = "attachment-reference-read-model";
|
|
1168
1175
|
var AttachmentReferenceReadModel = class extends BaseReadModel {
|
|
1169
1176
|
indexingQueue = Promise.resolve();
|
|
1177
|
+
checkpointTarget;
|
|
1178
|
+
/**
|
|
1179
|
+
* How far a replay may skip ahead of the cursor: the highest ordinal already
|
|
1180
|
+
* pulled from the index while the cursor was parked.
|
|
1181
|
+
*
|
|
1182
|
+
* NOT a claim that every ordinal below it committed -- the parked ordinal is
|
|
1183
|
+
* precisely the one that did not -- so it is a hint, and {@link replayFrom}
|
|
1184
|
+
* re-probes the gap before honouring it.
|
|
1185
|
+
*/
|
|
1186
|
+
replayedThrough;
|
|
1187
|
+
warnedCheckpoint;
|
|
1170
1188
|
constructor(db, operationIndex, writeCache, consistencyTracker, documentModelRegistry, schemaCompiler, referenceWriter) {
|
|
1171
1189
|
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
1172
1190
|
readModelId: ATTACHMENT_REFERENCE_READ_MODEL_ID,
|
|
@@ -1215,43 +1233,89 @@ var AttachmentReferenceReadModel = class extends BaseReadModel {
|
|
|
1215
1233
|
if (references.length > 0) await this.referenceWriter.addReferences(references);
|
|
1216
1234
|
}
|
|
1217
1235
|
async indexOperationsInOrdinalOrder(incoming) {
|
|
1218
|
-
|
|
1219
|
-
if (
|
|
1220
|
-
const incomingMax =
|
|
1221
|
-
|
|
1222
|
-
if (!this.isContiguousThrough(candidates, incomingMax)) {
|
|
1236
|
+
let candidates = this.sortAndDedupe(incoming);
|
|
1237
|
+
if (candidates.length === 0) return;
|
|
1238
|
+
const incomingMax = candidates[candidates.length - 1].context.ordinal;
|
|
1239
|
+
if (this.contiguousEnd(candidates) < incomingMax) {
|
|
1223
1240
|
const replayed = await this.loadThroughOrdinal(incomingMax);
|
|
1224
|
-
candidates = this.sortAndDedupe([...replayed, ...
|
|
1241
|
+
candidates = this.sortAndDedupe([...replayed, ...candidates]);
|
|
1225
1242
|
}
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
if (ordinal < expectedOrdinal) continue;
|
|
1231
|
-
if (ordinal > expectedOrdinal || ordinal > incomingMax) break;
|
|
1232
|
-
contiguous.push(item);
|
|
1233
|
-
expectedOrdinal++;
|
|
1243
|
+
const checkpoint = this.contiguousEnd(candidates);
|
|
1244
|
+
if (checkpoint < incomingMax && checkpoint !== this.warnedCheckpoint) {
|
|
1245
|
+
this.warnedCheckpoint = checkpoint;
|
|
1246
|
+
console.warn(`[${this.config.readModelId}] indexed through ordinal ${incomingMax} but parked the cursor at ${checkpoint}: ordinal ${checkpoint + 1} is missing`);
|
|
1234
1247
|
}
|
|
1235
|
-
if (expectedOrdinal <= incomingMax) throw new Error(`Attachment reference read model cannot advance past missing ordinal ${expectedOrdinal}`);
|
|
1236
1248
|
const previousOrdinal = this.lastOrdinal;
|
|
1249
|
+
this.checkpointTarget = checkpoint;
|
|
1237
1250
|
try {
|
|
1238
|
-
await super.indexOperations(
|
|
1251
|
+
await super.indexOperations(candidates);
|
|
1239
1252
|
} catch (error) {
|
|
1240
1253
|
this.lastOrdinal = previousOrdinal;
|
|
1254
|
+
this.replayedThrough = void 0;
|
|
1241
1255
|
throw error;
|
|
1256
|
+
} finally {
|
|
1257
|
+
this.checkpointTarget = void 0;
|
|
1242
1258
|
}
|
|
1259
|
+
if (checkpoint > previousOrdinal) this.replayedThrough = void 0;
|
|
1260
|
+
else if (checkpoint < incomingMax) this.replayedThrough = Math.max(this.replayedThrough ?? checkpoint, incomingMax);
|
|
1243
1261
|
}
|
|
1244
|
-
|
|
1262
|
+
/**
|
|
1263
|
+
* Writes the cursor parked by indexOperationsInOrdinalOrder instead of the
|
|
1264
|
+
* batch maximum, so indexing an operation above a gap never advances past it.
|
|
1265
|
+
*/
|
|
1266
|
+
async saveState(trx, items) {
|
|
1267
|
+
const target = this.checkpointTarget;
|
|
1268
|
+
if (target === void 0) {
|
|
1269
|
+
await super.saveState(trx, items);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
this.lastOrdinal = target;
|
|
1273
|
+
await trx.updateTable("ViewState").set({
|
|
1274
|
+
lastOrdinal: target,
|
|
1275
|
+
lastOperationTimestamp: /* @__PURE__ */ new Date()
|
|
1276
|
+
}).where("readModelId", "=", this.config.readModelId).execute();
|
|
1277
|
+
}
|
|
1278
|
+
/** Last ordinal of the contiguous run starting at lastOrdinal + 1. */
|
|
1279
|
+
contiguousEnd(items) {
|
|
1245
1280
|
let expectedOrdinal = this.lastOrdinal + 1;
|
|
1246
1281
|
for (const item of items) {
|
|
1247
|
-
|
|
1282
|
+
const ordinal = item.context.ordinal;
|
|
1283
|
+
if (ordinal < expectedOrdinal) continue;
|
|
1284
|
+
if (ordinal > expectedOrdinal) break;
|
|
1248
1285
|
expectedOrdinal++;
|
|
1249
1286
|
}
|
|
1250
|
-
return expectedOrdinal
|
|
1287
|
+
return expectedOrdinal - 1;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Opens a replay at the lowest ordinal still worth reading.
|
|
1291
|
+
*
|
|
1292
|
+
* {@link replayedThrough} is what keeps a permanently held hole -- a
|
|
1293
|
+
* rolled-back insert, which never fills -- from re-reading the whole tail on
|
|
1294
|
+
* every batch. It cannot be trusted on its own: a hole that fills without
|
|
1295
|
+
* being delivered here is visible only in the index, and a mark that is
|
|
1296
|
+
* never questioned would hide that operation for the life of the process.
|
|
1297
|
+
* That happens whenever another writer commits the gap (a second reactor on
|
|
1298
|
+
* the same database), and it leaves the cursor parked below an operation
|
|
1299
|
+
* whose references were never written -- `hasReference` then answers false
|
|
1300
|
+
* for an attachment that is genuinely referenced.
|
|
1301
|
+
*
|
|
1302
|
+
* So a parked batch spends one page probing the gap: the first row above the
|
|
1303
|
+
* cursor is the missing ordinal itself once it commits. Finding it drops the
|
|
1304
|
+
* mark and replays from the cursor; not finding it leaves the mark standing,
|
|
1305
|
+
* which is the cheap path and the common one.
|
|
1306
|
+
*/
|
|
1307
|
+
async replayFrom() {
|
|
1308
|
+
const fromCursor = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);
|
|
1309
|
+
if (this.replayedThrough === void 0) return fromCursor;
|
|
1310
|
+
if (fromCursor.results[0]?.context.ordinal === this.lastOrdinal + 1) {
|
|
1311
|
+
this.replayedThrough = void 0;
|
|
1312
|
+
return fromCursor;
|
|
1313
|
+
}
|
|
1314
|
+
return this.operationIndex.getSinceOrdinal(this.replayedThrough);
|
|
1251
1315
|
}
|
|
1252
1316
|
async loadThroughOrdinal(maxOrdinal) {
|
|
1253
1317
|
const operations = [];
|
|
1254
|
-
let page = await this.
|
|
1318
|
+
let page = await this.replayFrom();
|
|
1255
1319
|
for (;;) {
|
|
1256
1320
|
for (const item of page.results) if (item.context.ordinal <= maxOrdinal) operations.push(item);
|
|
1257
1321
|
if (page.results.some(({ context }) => context.ordinal >= maxOrdinal) || !page.next) break;
|
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/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/reference-index/attachment-schema-compiler.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 { 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 { 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 */\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 writer.destroy();\n await rm(tempPath, { force: true });\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 rm(tempPath, { force: true });\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 ): 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 const remote = await this.transport.fetch(hash, 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 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 const remote = await this.transport.fetch(hash, 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 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: pending reservation lookup and active reader tracking\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 { generatorTypeDefs } from \"@powerhousedao/document-engineering/graphql\";\nimport type {\n Action,\n DocumentModelModule,\n DocumentSpecification,\n OperationSpecification,\n} from \"@powerhousedao/shared/document-model\";\nimport { constantCase, pascalCase } from \"change-case\";\nimport {\n buildASTSchema,\n getNamedType,\n isInputObjectType,\n isListType,\n isNonNullType,\n Kind,\n parse,\n print,\n type DocumentNode,\n type GraphQLInputObjectType,\n type GraphQLInputType,\n type GraphQLSchema,\n} from \"graphql\";\nimport { parseRef } from \"../ref.js\";\nimport type {\n CompiledAttachmentExtractor,\n IAttachmentSchemaCompiler,\n} from \"./types.js\";\n\nconst ATTACHMENT_REF_TYPE = \"AttachmentRef\";\nconst CODEGEN_SCALAR_NAMES = new Set([\n \"Unknown\",\n \"DateTime\",\n \"Address\",\n ATTACHMENT_REF_TYPE,\n ...Object.keys(generatorTypeDefs as Record<string, string>),\n]);\n\ntype CompilerContext = {\n actionType: string;\n documentType: string;\n version: number;\n};\n\ntype ParsedOperation = {\n document: DocumentNode | null;\n operation: OperationSpecification;\n};\n\ntype ObjectPlan = {\n fields: FieldPlan[];\n};\n\ntype FieldPlan = {\n hasDefault: boolean;\n name: string;\n value: ValuePlan;\n};\n\ntype ReadFieldResult =\n | { present: false; value: undefined }\n | { present: true; value: unknown };\n\ntype ValuePlan =\n | {\n kind: \"attachment\";\n required: boolean;\n }\n | {\n item: ValuePlan;\n kind: \"list\";\n required: boolean;\n }\n | {\n body: ObjectPlan;\n kind: \"object\";\n required: boolean;\n };\n\nfunction describeContext(context: CompilerContext): string {\n return `document type \"${context.documentType}\", version ${context.version}, action \"${context.actionType}\"`;\n}\n\nfunction compilationError(context: CompilerContext, reason: string): Error {\n return new Error(\n `Attachment schema compilation failed for ${describeContext(context)}: ${reason}`,\n );\n}\n\nfunction extractionError(\n context: CompilerContext,\n path: string,\n reason: string,\n): Error {\n return new Error(\n `Attachment extraction failed for ${describeContext(context)} at ${path}: ${reason}`,\n );\n}\n\nfunction applicableSpecification(\n module: DocumentModelModule,\n context: CompilerContext,\n): DocumentSpecification {\n const matches = module.documentModel.global.specifications.filter(\n (specification) => specification.version === context.version,\n );\n if (matches.length !== 1) {\n throw compilationError(\n context,\n matches.length === 0\n ? \"the module has no matching specification\"\n : \"the module has multiple matching specifications\",\n );\n }\n return matches[0];\n}\n\nfunction parseOperations(\n specification: DocumentSpecification,\n context: CompilerContext,\n): ParsedOperation[] {\n return specification.modules.flatMap((moduleSpecification) =>\n moduleSpecification.operations.map((operation) => {\n if (operation.schema === null) return { document: null, operation };\n try {\n return { document: parse(operation.schema), operation };\n } catch {\n throw compilationError(context, \"an operation has invalid GraphQL SDL\");\n }\n }),\n );\n}\n\nfunction selectOperation(\n operations: ParsedOperation[],\n context: CompilerContext,\n): ParsedOperation | null {\n const matches = operations.filter(\n ({ operation }) =>\n operation.name !== null &&\n constantCase(operation.name) === context.actionType,\n );\n if (matches.length > 1) {\n throw compilationError(context, \"multiple operations map to the action\");\n }\n // Base/system actions (document creation, renames, undo) are not part of a\n // model's specification, so they cannot declare AttachmentRef fields.\n // They compile to the no-reference fast path instead of failing the stream.\n if (matches.length === 0) return null;\n return matches[0];\n}\n\nfunction buildEffectiveSchema(\n specification: DocumentSpecification,\n context: CompilerContext,\n): GraphQLSchema {\n const scalarSchemas = Array.from(\n CODEGEN_SCALAR_NAMES,\n (name) => `scalar ${name}`,\n );\n const stateSchemas = Object.values(specification.state).map(\n (state) => state.schema,\n );\n const operationSchemas = specification.modules.flatMap(\n (moduleSpecification) =>\n moduleSpecification.operations.flatMap((operation) =>\n operation.schema === null ? [] : [operation.schema],\n ),\n );\n\n try {\n const document = parse(\n [...scalarSchemas, ...stateSchemas, ...operationSchemas]\n .filter(Boolean)\n .join(\"\\n\\n\"),\n );\n return buildASTSchema(dedupeTypeDefinitions(document));\n } catch {\n throw compilationError(context, \"the effective GraphQL schema is invalid\");\n }\n}\n\n// Collapse identical repeats of a type (state schema vs operation schemas).\n// Conflicting or scalar duplicates stay in so the build still rejects them.\nfunction dedupeTypeDefinitions(document: DocumentNode): DocumentNode {\n const seen = new Map<string, string>();\n const definitions = document.definitions.filter((definition) => {\n if (\n !(\"name\" in definition) ||\n definition.name?.value === undefined ||\n definition.kind === Kind.SCALAR_TYPE_DEFINITION\n ) {\n return true;\n }\n const name = definition.name.value;\n const printed = print(definition);\n const existing = seen.get(name);\n if (existing === undefined) {\n seen.set(name, printed);\n return true;\n }\n return existing !== printed;\n });\n return { ...document, definitions };\n}\n\nfunction attachmentReachableTypes(\n definitions: Map<string, GraphQLInputObjectType>,\n): Set<string> {\n const reachable = new Set<string>();\n let changed = true;\n\n while (changed) {\n changed = false;\n for (const [name, definition] of definitions) {\n if (reachable.has(name)) continue;\n const reachesAttachment = Object.values(definition.getFields()).some(\n (field) => {\n const typeName = getNamedType(field.type).name;\n return (\n typeName === ATTACHMENT_REF_TYPE ||\n (definitions.has(typeName) && reachable.has(typeName))\n );\n },\n );\n if (reachesAttachment) {\n reachable.add(name);\n changed = true;\n }\n }\n }\n\n return reachable;\n}\n\nfunction compileValuePlan(\n type: GraphQLInputType,\n objectPlans: Map<string, ObjectPlan>,\n): ValuePlan {\n if (isNonNullType(type)) {\n return { ...compileValuePlan(type.ofType, objectPlans), required: true };\n }\n if (isListType(type)) {\n return {\n item: compileValuePlan(type.ofType, objectPlans),\n kind: \"list\",\n required: false,\n };\n }\n\n const typeName = type.name;\n if (typeName === ATTACHMENT_REF_TYPE) {\n return { kind: \"attachment\", required: false };\n }\n const body = objectPlans.get(typeName);\n if (!body) {\n throw new Error(`Internal attachment schema plan error for ${typeName}`);\n }\n return { body, kind: \"object\", required: false };\n}\n\nfunction compileRootPlan(\n rootName: string,\n definitions: Map<string, GraphQLInputObjectType>,\n): ObjectPlan | null {\n const reachable = attachmentReachableTypes(definitions);\n if (!reachable.has(rootName)) return null;\n\n const objectPlans = new Map<string, ObjectPlan>();\n for (const name of reachable) objectPlans.set(name, { fields: [] });\n\n for (const name of reachable) {\n const definition = definitions.get(name);\n const body = objectPlans.get(name);\n if (!definition || !body) continue;\n for (const field of Object.values(definition.getFields())) {\n const typeName = getNamedType(field.type).name;\n if (typeName !== ATTACHMENT_REF_TYPE && !reachable.has(typeName)) {\n continue;\n }\n body.fields.push({\n hasDefault: field.defaultValue !== undefined,\n name: field.name,\n value: compileValuePlan(field.type, objectPlans),\n });\n }\n }\n\n return objectPlans.get(rootName) ?? null;\n}\n\nfunction readOwnField(\n value: Record<string, unknown>,\n fieldName: string,\n context: CompilerContext,\n path: string,\n): ReadFieldResult {\n try {\n if (!Object.prototype.hasOwnProperty.call(value, fieldName)) {\n return { present: false, value: undefined };\n }\n return { present: true, value: value[fieldName] };\n } catch {\n throw extractionError(context, path, \"the declared field cannot be read\");\n }\n}\n\nfunction extractValue(\n plan: ValuePlan,\n value: unknown,\n path: string,\n context: CompilerContext,\n refs: AttachmentRef[],\n seenRefs: Set<string>,\n activeObjects: WeakSet<object>,\n): void {\n if (value === null || value === undefined) {\n if (plan.required) {\n throw extractionError(\n context,\n path,\n \"a required value is missing or null\",\n );\n }\n return;\n }\n\n if (plan.kind === \"attachment\") {\n if (typeof value !== \"string\") {\n throw extractionError(context, path, \"expected an AttachmentRef string\");\n }\n try {\n parseRef(value as AttachmentRef);\n } catch {\n throw extractionError(context, path, \"the AttachmentRef is malformed\");\n }\n if (!seenRefs.has(value)) {\n seenRefs.add(value);\n refs.push(value as AttachmentRef);\n }\n return;\n }\n\n if (plan.kind === \"list\") {\n if (!Array.isArray(value)) {\n throw extractionError(context, path, \"expected a list\");\n }\n for (let index = 0; index < value.length; index += 1) {\n extractValue(\n plan.item,\n value[index],\n `${path}[${index}]`,\n context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n return;\n }\n\n if (typeof value !== \"object\" || Array.isArray(value)) {\n throw extractionError(context, path, \"expected an input object\");\n }\n if (activeObjects.has(value)) {\n throw extractionError(context, path, \"the input value contains a cycle\");\n }\n\n activeObjects.add(value);\n try {\n const record = value as Record<string, unknown>;\n for (const field of plan.body.fields) {\n const fieldPath = `${path}.${field.name}`;\n const result = readOwnField(record, field.name, context, fieldPath);\n if ((!result.present || result.value === undefined) && field.hasDefault) {\n continue;\n }\n extractValue(\n field.value,\n result.value,\n fieldPath,\n context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n } finally {\n activeObjects.delete(value);\n }\n}\n\nclass SchemaCompiledAttachmentExtractor implements CompiledAttachmentExtractor {\n constructor(\n private readonly context: CompilerContext,\n private readonly rootPlan: ObjectPlan | null,\n ) {}\n\n extract(action: Action): AttachmentRef[] {\n if (action.type !== this.context.actionType) {\n throw extractionError(\n this.context,\n \"input\",\n \"the action type does not match the compiled schema\",\n );\n }\n if (!this.rootPlan) return [];\n if (\n action.input === null ||\n typeof action.input !== \"object\" ||\n Array.isArray(action.input)\n ) {\n throw extractionError(this.context, \"input\", \"expected an input object\");\n }\n\n const refs: AttachmentRef[] = [];\n const seenRefs = new Set<string>();\n const activeObjects = new WeakSet<object>();\n activeObjects.add(action.input);\n try {\n const input = action.input as Record<string, unknown>;\n for (const field of this.rootPlan.fields) {\n const path = `input.${field.name}`;\n const result = readOwnField(input, field.name, this.context, path);\n if (\n (!result.present || result.value === undefined) &&\n field.hasDefault\n ) {\n continue;\n }\n extractValue(\n field.value,\n result.value,\n path,\n this.context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n } finally {\n activeObjects.delete(action.input);\n }\n return refs;\n }\n}\n\nfunction compileExtractor(\n module: DocumentModelModule,\n actionType: string,\n): CompiledAttachmentExtractor {\n const context: CompilerContext = {\n actionType,\n documentType: module.documentModel.global.id,\n version: module.version ?? 1,\n };\n const specification = applicableSpecification(module, context);\n const operations = parseOperations(specification, context);\n const selected = selectOperation(operations, context);\n if (selected === null || selected.operation.schema === null) {\n return new SchemaCompiledAttachmentExtractor(context, null);\n }\n const effectiveSchema = buildEffectiveSchema(specification, context);\n\n const operationName = selected.operation.name;\n if (operationName === null) {\n throw compilationError(context, \"the operation has no name\");\n }\n const rootName = `${pascalCase(operationName)}Input`;\n const definitions = new Map<string, GraphQLInputObjectType>();\n for (const type of Object.values(effectiveSchema.getTypeMap())) {\n if (isInputObjectType(type) && !type.name.startsWith(\"__\")) {\n definitions.set(type.name, type);\n }\n }\n const rootDefinitions = selected.document?.definitions.filter(\n (definition) =>\n (definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION ||\n definition.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION) &&\n definition.name.value === rootName,\n );\n if (!rootDefinitions?.length || !definitions.has(rootName)) {\n throw compilationError(\n context,\n `the operation does not declare its expected root input \"${rootName}\"`,\n );\n }\n\n return new SchemaCompiledAttachmentExtractor(\n context,\n compileRootPlan(rootName, definitions),\n );\n}\n\nexport class AttachmentSchemaCompiler implements IAttachmentSchemaCompiler {\n private readonly cache = new WeakMap<\n DocumentModelModule,\n Map<string, CompiledAttachmentExtractor>\n >();\n\n forModuleAction(\n module: DocumentModelModule,\n actionType: string,\n ): CompiledAttachmentExtractor {\n let moduleCache = this.cache.get(module);\n if (!moduleCache) {\n moduleCache = new Map();\n this.cache.set(module, moduleCache);\n }\n\n const cached = moduleCache.get(actionType);\n if (cached) return cached;\n\n const compiled = compileExtractor(module, actionType);\n moduleCache.set(actionType, compiled);\n return compiled;\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 IAttachmentReferenceReader,\n IAttachmentReferenceWriter,\n} from \"./types.js\";\n\nexport class KyselyAttachmentReferenceStore\n implements IAttachmentReferenceReader, 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 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","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 type DocumentViewDatabase,\n type IConsistencyTracker,\n type IDocumentModelRegistry,\n type IOperationIndex,\n type IWriteCache,\n} from \"@powerhousedao/reactor\";\nimport type { OperationWithContext } 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\nexport class AttachmentReferenceReadModel extends BaseReadModel {\n private indexingQueue: Promise<void> = Promise.resolve();\n\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 });\n }\n\n override indexOperations(items: OperationWithContext[]): Promise<void> {\n return this.enqueue(() => this.indexOperationsInOrdinalOrder(items));\n }\n\n override init(): Promise<void> {\n return this.enqueue(async () => {\n const viewState = await this.loadState();\n\n if (viewState !== undefined) {\n this.lastOrdinal = viewState;\n } else {\n await this.initializeState();\n }\n\n let page = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);\n while (page.results.length > 0) {\n await this.indexOperationsInOrdinalOrder(page.results);\n if (!page.next) break;\n page = await page.next();\n }\n });\n }\n\n private enqueue(work: () => Promise<void>): Promise<void> {\n const result = this.indexingQueue.then(work);\n this.indexingQueue = result.catch(() => undefined);\n return result;\n }\n\n protected override async commitOperations(\n items: OperationWithContext[],\n ): Promise<void> {\n const references: AttachmentReferenceInput[] = [];\n\n for (const { operation, context } of items) {\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 }\n\n private async indexOperationsInOrdinalOrder(\n incoming: OperationWithContext[],\n ): Promise<void> {\n const pending = this.sortAndDedupe(\n incoming.filter(({ context }) => context.ordinal > this.lastOrdinal),\n );\n if (pending.length === 0) return;\n\n const incomingMax = pending[pending.length - 1]!.context.ordinal;\n let candidates = pending;\n\n if (!this.isContiguousThrough(candidates, incomingMax)) {\n const replayed = await this.loadThroughOrdinal(incomingMax);\n candidates = this.sortAndDedupe([...replayed, ...pending]);\n }\n\n const contiguous: OperationWithContext[] = [];\n let expectedOrdinal = this.lastOrdinal + 1;\n for (const item of candidates) {\n const ordinal = item.context.ordinal;\n if (ordinal < expectedOrdinal) continue;\n if (ordinal > expectedOrdinal || ordinal > incomingMax) break;\n contiguous.push(item);\n expectedOrdinal++;\n }\n\n if (expectedOrdinal <= incomingMax) {\n throw new Error(\n `Attachment reference read model cannot advance past missing ordinal ${expectedOrdinal}`,\n );\n }\n\n const previousOrdinal = this.lastOrdinal;\n try {\n await super.indexOperations(contiguous);\n } catch (error) {\n this.lastOrdinal = previousOrdinal;\n throw error;\n }\n }\n\n private isContiguousThrough(\n items: OperationWithContext[],\n maxOrdinal: number,\n ): boolean {\n let expectedOrdinal = this.lastOrdinal + 1;\n for (const item of items) {\n if (item.context.ordinal !== expectedOrdinal) return false;\n expectedOrdinal++;\n }\n return expectedOrdinal > maxOrdinal;\n }\n\n private async loadThroughOrdinal(\n maxOrdinal: number,\n ): Promise<OperationWithContext[]> {\n const operations: OperationWithContext[] = [];\n let page = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);\n\n for (;;) {\n for (const item of page.results) {\n if (item.context.ordinal <= maxOrdinal) operations.push(item);\n }\n if (\n page.results.some(({ context }) => context.ordinal >= maxOrdinal) ||\n !page.next\n ) {\n break;\n }\n page = await page.next();\n }\n\n return operations;\n }\n\n private sortAndDedupe(items: OperationWithContext[]): OperationWithContext[] {\n const byOrdinal = new Map<number, OperationWithContext>();\n for (const item of items) {\n byOrdinal.set(item.context.ordinal, item);\n }\n return [...byOrdinal.values()].sort(\n (left, right) => left.context.ordinal - right.context.ordinal,\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/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/reference-index/attachment-schema-compiler.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 { 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 ): 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 const remote = await this.transport.fetch(hash, 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 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 const remote = await this.transport.fetch(hash, 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 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: pending reservation lookup and active reader tracking\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 { generatorTypeDefs } from \"@powerhousedao/document-engineering/graphql\";\nimport type {\n Action,\n DocumentModelModule,\n DocumentSpecification,\n OperationSpecification,\n} from \"@powerhousedao/shared/document-model\";\nimport { constantCase, pascalCase } from \"change-case\";\nimport {\n buildASTSchema,\n getNamedType,\n isInputObjectType,\n isListType,\n isNonNullType,\n Kind,\n parse,\n print,\n type DocumentNode,\n type GraphQLInputObjectType,\n type GraphQLInputType,\n type GraphQLSchema,\n} from \"graphql\";\nimport { parseRef } from \"../ref.js\";\nimport type {\n CompiledAttachmentExtractor,\n IAttachmentSchemaCompiler,\n} from \"./types.js\";\n\nconst ATTACHMENT_REF_TYPE = \"AttachmentRef\";\nconst CODEGEN_SCALAR_NAMES = new Set([\n \"Unknown\",\n \"DateTime\",\n \"Address\",\n ATTACHMENT_REF_TYPE,\n ...Object.keys(generatorTypeDefs as Record<string, string>),\n]);\n\ntype CompilerContext = {\n actionType: string;\n documentType: string;\n version: number;\n};\n\ntype ParsedOperation = {\n document: DocumentNode | null;\n operation: OperationSpecification;\n};\n\ntype ObjectPlan = {\n fields: FieldPlan[];\n};\n\ntype FieldPlan = {\n hasDefault: boolean;\n name: string;\n value: ValuePlan;\n};\n\ntype ReadFieldResult =\n | { present: false; value: undefined }\n | { present: true; value: unknown };\n\ntype ValuePlan =\n | {\n kind: \"attachment\";\n required: boolean;\n }\n | {\n item: ValuePlan;\n kind: \"list\";\n required: boolean;\n }\n | {\n body: ObjectPlan;\n kind: \"object\";\n required: boolean;\n };\n\nfunction describeContext(context: CompilerContext): string {\n return `document type \"${context.documentType}\", version ${context.version}, action \"${context.actionType}\"`;\n}\n\nfunction compilationError(context: CompilerContext, reason: string): Error {\n return new Error(\n `Attachment schema compilation failed for ${describeContext(context)}: ${reason}`,\n );\n}\n\nfunction extractionError(\n context: CompilerContext,\n path: string,\n reason: string,\n): Error {\n return new Error(\n `Attachment extraction failed for ${describeContext(context)} at ${path}: ${reason}`,\n );\n}\n\nfunction applicableSpecification(\n module: DocumentModelModule,\n context: CompilerContext,\n): DocumentSpecification {\n const matches = module.documentModel.global.specifications.filter(\n (specification) => specification.version === context.version,\n );\n if (matches.length !== 1) {\n throw compilationError(\n context,\n matches.length === 0\n ? \"the module has no matching specification\"\n : \"the module has multiple matching specifications\",\n );\n }\n return matches[0];\n}\n\nfunction parseOperations(\n specification: DocumentSpecification,\n context: CompilerContext,\n): ParsedOperation[] {\n return specification.modules.flatMap((moduleSpecification) =>\n moduleSpecification.operations.map((operation) => {\n if (operation.schema === null) return { document: null, operation };\n try {\n return { document: parse(operation.schema), operation };\n } catch {\n throw compilationError(context, \"an operation has invalid GraphQL SDL\");\n }\n }),\n );\n}\n\nfunction selectOperation(\n operations: ParsedOperation[],\n context: CompilerContext,\n): ParsedOperation | null {\n const matches = operations.filter(\n ({ operation }) =>\n operation.name !== null &&\n constantCase(operation.name) === context.actionType,\n );\n if (matches.length > 1) {\n throw compilationError(context, \"multiple operations map to the action\");\n }\n // Base/system actions (document creation, renames, undo) are not part of a\n // model's specification, so they cannot declare AttachmentRef fields.\n // They compile to the no-reference fast path instead of failing the stream.\n if (matches.length === 0) return null;\n return matches[0];\n}\n\nfunction buildEffectiveSchema(\n specification: DocumentSpecification,\n context: CompilerContext,\n): GraphQLSchema {\n const scalarSchemas = Array.from(\n CODEGEN_SCALAR_NAMES,\n (name) => `scalar ${name}`,\n );\n const stateSchemas = Object.values(specification.state).map(\n (state) => state.schema,\n );\n const operationSchemas = specification.modules.flatMap(\n (moduleSpecification) =>\n moduleSpecification.operations.flatMap((operation) =>\n operation.schema === null ? [] : [operation.schema],\n ),\n );\n\n try {\n const document = parse(\n [...scalarSchemas, ...stateSchemas, ...operationSchemas]\n .filter(Boolean)\n .join(\"\\n\\n\"),\n );\n return buildASTSchema(dedupeTypeDefinitions(document));\n } catch {\n throw compilationError(context, \"the effective GraphQL schema is invalid\");\n }\n}\n\n// Collapse identical repeats of a type (state schema vs operation schemas).\n// Conflicting or scalar duplicates stay in so the build still rejects them.\nfunction dedupeTypeDefinitions(document: DocumentNode): DocumentNode {\n const seen = new Map<string, string>();\n const definitions = document.definitions.filter((definition) => {\n if (\n !(\"name\" in definition) ||\n definition.name?.value === undefined ||\n definition.kind === Kind.SCALAR_TYPE_DEFINITION\n ) {\n return true;\n }\n const name = definition.name.value;\n const printed = print(definition);\n const existing = seen.get(name);\n if (existing === undefined) {\n seen.set(name, printed);\n return true;\n }\n return existing !== printed;\n });\n return { ...document, definitions };\n}\n\nfunction attachmentReachableTypes(\n definitions: Map<string, GraphQLInputObjectType>,\n): Set<string> {\n const reachable = new Set<string>();\n let changed = true;\n\n while (changed) {\n changed = false;\n for (const [name, definition] of definitions) {\n if (reachable.has(name)) continue;\n const reachesAttachment = Object.values(definition.getFields()).some(\n (field) => {\n const typeName = getNamedType(field.type).name;\n return (\n typeName === ATTACHMENT_REF_TYPE ||\n (definitions.has(typeName) && reachable.has(typeName))\n );\n },\n );\n if (reachesAttachment) {\n reachable.add(name);\n changed = true;\n }\n }\n }\n\n return reachable;\n}\n\nfunction compileValuePlan(\n type: GraphQLInputType,\n objectPlans: Map<string, ObjectPlan>,\n): ValuePlan {\n if (isNonNullType(type)) {\n return { ...compileValuePlan(type.ofType, objectPlans), required: true };\n }\n if (isListType(type)) {\n return {\n item: compileValuePlan(type.ofType, objectPlans),\n kind: \"list\",\n required: false,\n };\n }\n\n const typeName = type.name;\n if (typeName === ATTACHMENT_REF_TYPE) {\n return { kind: \"attachment\", required: false };\n }\n const body = objectPlans.get(typeName);\n if (!body) {\n throw new Error(`Internal attachment schema plan error for ${typeName}`);\n }\n return { body, kind: \"object\", required: false };\n}\n\nfunction compileRootPlan(\n rootName: string,\n definitions: Map<string, GraphQLInputObjectType>,\n): ObjectPlan | null {\n const reachable = attachmentReachableTypes(definitions);\n if (!reachable.has(rootName)) return null;\n\n const objectPlans = new Map<string, ObjectPlan>();\n for (const name of reachable) objectPlans.set(name, { fields: [] });\n\n for (const name of reachable) {\n const definition = definitions.get(name);\n const body = objectPlans.get(name);\n if (!definition || !body) continue;\n for (const field of Object.values(definition.getFields())) {\n const typeName = getNamedType(field.type).name;\n if (typeName !== ATTACHMENT_REF_TYPE && !reachable.has(typeName)) {\n continue;\n }\n body.fields.push({\n hasDefault: field.defaultValue !== undefined,\n name: field.name,\n value: compileValuePlan(field.type, objectPlans),\n });\n }\n }\n\n return objectPlans.get(rootName) ?? null;\n}\n\nfunction readOwnField(\n value: Record<string, unknown>,\n fieldName: string,\n context: CompilerContext,\n path: string,\n): ReadFieldResult {\n try {\n if (!Object.prototype.hasOwnProperty.call(value, fieldName)) {\n return { present: false, value: undefined };\n }\n return { present: true, value: value[fieldName] };\n } catch {\n throw extractionError(context, path, \"the declared field cannot be read\");\n }\n}\n\nfunction extractValue(\n plan: ValuePlan,\n value: unknown,\n path: string,\n context: CompilerContext,\n refs: AttachmentRef[],\n seenRefs: Set<string>,\n activeObjects: WeakSet<object>,\n): void {\n if (value === null || value === undefined) {\n if (plan.required) {\n throw extractionError(\n context,\n path,\n \"a required value is missing or null\",\n );\n }\n return;\n }\n\n if (plan.kind === \"attachment\") {\n if (typeof value !== \"string\") {\n throw extractionError(context, path, \"expected an AttachmentRef string\");\n }\n try {\n parseRef(value as AttachmentRef);\n } catch {\n throw extractionError(context, path, \"the AttachmentRef is malformed\");\n }\n if (!seenRefs.has(value)) {\n seenRefs.add(value);\n refs.push(value as AttachmentRef);\n }\n return;\n }\n\n if (plan.kind === \"list\") {\n if (!Array.isArray(value)) {\n throw extractionError(context, path, \"expected a list\");\n }\n for (let index = 0; index < value.length; index += 1) {\n extractValue(\n plan.item,\n value[index],\n `${path}[${index}]`,\n context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n return;\n }\n\n if (typeof value !== \"object\" || Array.isArray(value)) {\n throw extractionError(context, path, \"expected an input object\");\n }\n if (activeObjects.has(value)) {\n throw extractionError(context, path, \"the input value contains a cycle\");\n }\n\n activeObjects.add(value);\n try {\n const record = value as Record<string, unknown>;\n for (const field of plan.body.fields) {\n const fieldPath = `${path}.${field.name}`;\n const result = readOwnField(record, field.name, context, fieldPath);\n if ((!result.present || result.value === undefined) && field.hasDefault) {\n continue;\n }\n extractValue(\n field.value,\n result.value,\n fieldPath,\n context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n } finally {\n activeObjects.delete(value);\n }\n}\n\nclass SchemaCompiledAttachmentExtractor implements CompiledAttachmentExtractor {\n constructor(\n private readonly context: CompilerContext,\n private readonly rootPlan: ObjectPlan | null,\n ) {}\n\n extract(action: Action): AttachmentRef[] {\n if (action.type !== this.context.actionType) {\n throw extractionError(\n this.context,\n \"input\",\n \"the action type does not match the compiled schema\",\n );\n }\n if (!this.rootPlan) return [];\n if (\n action.input === null ||\n typeof action.input !== \"object\" ||\n Array.isArray(action.input)\n ) {\n throw extractionError(this.context, \"input\", \"expected an input object\");\n }\n\n const refs: AttachmentRef[] = [];\n const seenRefs = new Set<string>();\n const activeObjects = new WeakSet<object>();\n activeObjects.add(action.input);\n try {\n const input = action.input as Record<string, unknown>;\n for (const field of this.rootPlan.fields) {\n const path = `input.${field.name}`;\n const result = readOwnField(input, field.name, this.context, path);\n if (\n (!result.present || result.value === undefined) &&\n field.hasDefault\n ) {\n continue;\n }\n extractValue(\n field.value,\n result.value,\n path,\n this.context,\n refs,\n seenRefs,\n activeObjects,\n );\n }\n } finally {\n activeObjects.delete(action.input);\n }\n return refs;\n }\n}\n\nfunction compileExtractor(\n module: DocumentModelModule,\n actionType: string,\n): CompiledAttachmentExtractor {\n const context: CompilerContext = {\n actionType,\n documentType: module.documentModel.global.id,\n version: module.version ?? 1,\n };\n const specification = applicableSpecification(module, context);\n const operations = parseOperations(specification, context);\n const selected = selectOperation(operations, context);\n if (selected === null || selected.operation.schema === null) {\n return new SchemaCompiledAttachmentExtractor(context, null);\n }\n const effectiveSchema = buildEffectiveSchema(specification, context);\n\n const operationName = selected.operation.name;\n if (operationName === null) {\n throw compilationError(context, \"the operation has no name\");\n }\n const rootName = `${pascalCase(operationName)}Input`;\n const definitions = new Map<string, GraphQLInputObjectType>();\n for (const type of Object.values(effectiveSchema.getTypeMap())) {\n if (isInputObjectType(type) && !type.name.startsWith(\"__\")) {\n definitions.set(type.name, type);\n }\n }\n const rootDefinitions = selected.document?.definitions.filter(\n (definition) =>\n (definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION ||\n definition.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION) &&\n definition.name.value === rootName,\n );\n if (!rootDefinitions?.length || !definitions.has(rootName)) {\n throw compilationError(\n context,\n `the operation does not declare its expected root input \"${rootName}\"`,\n );\n }\n\n return new SchemaCompiledAttachmentExtractor(\n context,\n compileRootPlan(rootName, definitions),\n );\n}\n\nexport class AttachmentSchemaCompiler implements IAttachmentSchemaCompiler {\n private readonly cache = new WeakMap<\n DocumentModelModule,\n Map<string, CompiledAttachmentExtractor>\n >();\n\n forModuleAction(\n module: DocumentModelModule,\n actionType: string,\n ): CompiledAttachmentExtractor {\n let moduleCache = this.cache.get(module);\n if (!moduleCache) {\n moduleCache = new Map();\n this.cache.set(module, moduleCache);\n }\n\n const cached = moduleCache.get(actionType);\n if (cached) return cached;\n\n const compiled = compileExtractor(module, actionType);\n moduleCache.set(actionType, compiled);\n return compiled;\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 IAttachmentReferenceReader,\n IAttachmentReferenceWriter,\n} from \"./types.js\";\n\nexport class KyselyAttachmentReferenceStore\n implements IAttachmentReferenceReader, 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 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","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 type DocumentViewDatabase,\n type IConsistencyTracker,\n type IDocumentModelRegistry,\n type IOperationIndex,\n type IWriteCache,\n type PagedResults,\n} from \"@powerhousedao/reactor\";\nimport type { OperationWithContext } from \"@powerhousedao/shared/document-model\";\nimport type { Kysely, Transaction } 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\nexport class AttachmentReferenceReadModel extends BaseReadModel {\n private indexingQueue: Promise<void> = Promise.resolve();\n private checkpointTarget: number | undefined;\n /**\n * How far a replay may skip ahead of the cursor: the highest ordinal already\n * pulled from the index while the cursor was parked.\n *\n * NOT a claim that every ordinal below it committed -- the parked ordinal is\n * precisely the one that did not -- so it is a hint, and {@link replayFrom}\n * re-probes the gap before honouring it.\n */\n private replayedThrough: number | undefined;\n private warnedCheckpoint: number | undefined;\n\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 });\n }\n\n override indexOperations(items: OperationWithContext[]): Promise<void> {\n return this.enqueue(() => this.indexOperationsInOrdinalOrder(items));\n }\n\n override init(): Promise<void> {\n return this.enqueue(async () => {\n const viewState = await this.loadState();\n\n if (viewState !== undefined) {\n this.lastOrdinal = viewState;\n } else {\n await this.initializeState();\n }\n\n let page = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);\n while (page.results.length > 0) {\n await this.indexOperationsInOrdinalOrder(page.results);\n if (!page.next) break;\n page = await page.next();\n }\n });\n }\n\n private enqueue(work: () => Promise<void>): Promise<void> {\n const result = this.indexingQueue.then(work);\n this.indexingQueue = result.catch(() => undefined);\n return result;\n }\n\n protected override async commitOperations(\n items: OperationWithContext[],\n ): Promise<void> {\n const references: AttachmentReferenceInput[] = [];\n\n for (const { operation, context } of items) {\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 }\n\n private async indexOperationsInOrdinalOrder(\n incoming: OperationWithContext[],\n ): Promise<void> {\n let candidates = this.sortAndDedupe(incoming);\n if (candidates.length === 0) return;\n\n const incomingMax = candidates[candidates.length - 1]!.context.ordinal;\n\n if (this.contiguousEnd(candidates) < incomingMax) {\n const replayed = await this.loadThroughOrdinal(incomingMax);\n candidates = this.sortAndDedupe([...replayed, ...candidates]);\n }\n\n // The ordinal sequence is a Postgres serial, so it has permanent holes\n // (rolled-back inserts) and transient ones (still-open transactions).\n // Index everything delivered, but park the cursor at the end of the\n // contiguous run so a gap that later fills is still replayed. Re-indexing\n // is idempotent, so a conservative cursor only costs repeated work.\n const checkpoint = this.contiguousEnd(candidates);\n if (checkpoint < incomingMax && checkpoint !== this.warnedCheckpoint) {\n this.warnedCheckpoint = checkpoint;\n console.warn(\n `[${this.config.readModelId}] indexed through ordinal ${incomingMax} ` +\n `but parked the cursor at ${checkpoint}: ordinal ${checkpoint + 1} is missing`,\n );\n }\n\n const previousOrdinal = this.lastOrdinal;\n this.checkpointTarget = checkpoint;\n try {\n await super.indexOperations(candidates);\n } catch (error) {\n this.lastOrdinal = previousOrdinal;\n // A failed batch leaves its range uncommitted, so the mark cannot stand.\n this.replayedThrough = undefined;\n throw error;\n } finally {\n this.checkpointTarget = undefined;\n }\n\n // A parked cursor makes every later batch non-contiguous; without this mark\n // each replay would restart at the hole and grow without bound.\n if (checkpoint > previousOrdinal) {\n this.replayedThrough = undefined;\n } else if (checkpoint < incomingMax) {\n this.replayedThrough = Math.max(\n this.replayedThrough ?? checkpoint,\n incomingMax,\n );\n }\n }\n\n /**\n * Writes the cursor parked by indexOperationsInOrdinalOrder instead of the\n * batch maximum, so indexing an operation above a gap never advances past it.\n */\n protected override async saveState(\n trx: Transaction<DocumentViewDatabase>,\n items: OperationWithContext[],\n ): Promise<void> {\n const target = this.checkpointTarget;\n if (target === undefined) {\n await super.saveState(trx, items);\n return;\n }\n\n this.lastOrdinal = target;\n await trx\n .updateTable(\"ViewState\")\n .set({\n lastOrdinal: target,\n lastOperationTimestamp: new Date(),\n })\n .where(\"readModelId\", \"=\", this.config.readModelId)\n .execute();\n }\n\n /** Last ordinal of the contiguous run starting at lastOrdinal + 1. */\n private contiguousEnd(items: OperationWithContext[]): number {\n let expectedOrdinal = this.lastOrdinal + 1;\n for (const item of items) {\n const ordinal = item.context.ordinal;\n if (ordinal < expectedOrdinal) continue;\n if (ordinal > expectedOrdinal) break;\n expectedOrdinal++;\n }\n return expectedOrdinal - 1;\n }\n\n /**\n * Opens a replay at the lowest ordinal still worth reading.\n *\n * {@link replayedThrough} is what keeps a permanently held hole -- a\n * rolled-back insert, which never fills -- from re-reading the whole tail on\n * every batch. It cannot be trusted on its own: a hole that fills without\n * being delivered here is visible only in the index, and a mark that is\n * never questioned would hide that operation for the life of the process.\n * That happens whenever another writer commits the gap (a second reactor on\n * the same database), and it leaves the cursor parked below an operation\n * whose references were never written -- `hasReference` then answers false\n * for an attachment that is genuinely referenced.\n *\n * So a parked batch spends one page probing the gap: the first row above the\n * cursor is the missing ordinal itself once it commits. Finding it drops the\n * mark and replays from the cursor; not finding it leaves the mark standing,\n * which is the cheap path and the common one.\n */\n private async replayFrom(): Promise<PagedResults<OperationWithContext>> {\n const fromCursor = await this.operationIndex.getSinceOrdinal(\n this.lastOrdinal,\n );\n if (this.replayedThrough === undefined) {\n return fromCursor;\n }\n\n // Ordinals come back ascending, so the first row answers it outright.\n if (fromCursor.results[0]?.context.ordinal === this.lastOrdinal + 1) {\n this.replayedThrough = undefined;\n return fromCursor;\n }\n\n return this.operationIndex.getSinceOrdinal(this.replayedThrough);\n }\n\n private async loadThroughOrdinal(\n maxOrdinal: number,\n ): Promise<OperationWithContext[]> {\n const operations: OperationWithContext[] = [];\n let page = await this.replayFrom();\n\n for (;;) {\n for (const item of page.results) {\n if (item.context.ordinal <= maxOrdinal) operations.push(item);\n }\n if (\n page.results.some(({ context }) => context.ordinal >= maxOrdinal) ||\n !page.next\n ) {\n break;\n }\n page = await page.next();\n }\n\n return operations;\n }\n\n private sortAndDedupe(items: OperationWithContext[]): OperationWithContext[] {\n const byOrdinal = new Map<number, OperationWithContext>();\n for (const item of items) {\n byOrdinal.set(item.context.ordinal, item);\n }\n return [...byOrdinal.values()].sort(\n (left, right) => left.context.ordinal - right.context.ordinal,\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBAAoB,MAAsB;AACxD,QAAO,KAAK,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;;;;;;;;;;;;;;;;AAqBvD,eAAe,gBACb,QACA,UACe;AACf,QAAO,SAAS;AAChB,KAAI;AACF,QAAM,SAAS,QAAQ,EAAE,OAAO,OAAO,CAAC;SAClC;AAGR,OAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;;AAGrC,eAAsB,qBACpB,MACA,MACiB;AACjB,OAAM,MAAM,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;CAE/C,MAAM,WAAW,KAAK,QAAQ,KAAK,EAAE,GAAG,YAAY,CAAC,MAAM;CAC3D,MAAM,SAAS,kBAAkB,SAAS;CAC1C,MAAM,SAAS,KAAK,WAAW;CAC/B,IAAI,eAAe;CACnB,IAAI;CAKJ,MAAM,eAAe,QAAiB;AACpC,kBAAgB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC;;AAErE,QAAO,GAAG,SAAS,YAAY;AAE/B,KAAI;AACF,WAAS;GACP,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,OAAI,KAAM;AAGV,OAAI,YAAa;AACjB,mBAAgB,MAAM;AAEtB,OAAI,CADgB,OAAO,MAAM,MAAM,CAErC,OAAM,IAAI,SAAe,SAAS,WAAW;IAC3C,MAAM,gBAAgB;AACpB,YAAO,IAAI,SAAS,QAAQ;AAC5B,cAAS;;IAEX,MAAM,WAAW,QAAe;AAC9B,YAAO,IAAI,SAAS,QAAQ;AAC5B,YAAO,IAAI;;AAEb,WAAO,KAAK,SAAS,QAAQ;AAC7B,WAAO,KAAK,SAAS,QAAQ;KAC7B;;UAGC,KAAK;AACZ,cAAY,IAAI;WACR;AACR,SAAO,aAAa;;AAGtB,KAAI,aAAa;AACf,QAAM,gBAAgB,QAAQ,SAAS;AACvC,QAAM;;AAGR,KAAI;AACF,QAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,UAAO,KAAK,QAAuB;AACjC,QAAI,IAAK,QAAO,IAAI;QACf,UAAS;KACd;AACF,UAAO,KAAK,SAAS,OAAO;IAC5B;AACF,QAAM,OAAO,UAAU,KAAK;UACrB,KAAK;AACZ,QAAM,gBAAgB,QAAQ,SAAS;AACvC,QAAM,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC;;AAG3D,QAAO;;;;;AAMT,SAAgB,qBAAqB,MAA0C;CAC7E,MAAM,aAAa,iBAAiB,KAAK;AACzC,QAAO,SAAS,MAAM,WAAW;;;;;AAMnC,eAAsB,sBAAsB,MAA6B;AACvE,OAAM,GAAG,MAAM,EAAE,OAAO,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAiDjC,eAAsB,mBACpB,UACA,MACA,UAII,EAAE,EAC0D;CAChE,MAAM,EAAE,UAAU,mBAAmB,WAAW;AAEhD,SAAQ,gBAAgB;CACxB,MAAM,SAAS,KAAK,UAAU,OAAO;AACrC,OAAM,MAAM,QAAQ,EAAE,WAAW,MAAM,CAAC;CACxC,MAAM,WAAW,KAAK,QAAQ,YAAY,CAAC;CAE3C,MAAM,SAAS,WAAW,SAAS;CACnC,MAAM,SAAS,kBAAkB,SAAS;CAC1C,MAAM,SAAS,KAAK,WAAW;CAK/B,MAAM,gBAAgB;AACpB,SAAO,OAAO,QAAQ,OAAO,CAAC,YAAY,GAAG;;AAE/C,SAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;CAC1D,IAAI,YAAY;CAChB,IAAI;AAEJ,KAAI;AACF,WAAS;AACP,WAAQ,gBAAgB;GACxB,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,OAAI,KAAM;AACV,gBAAa,MAAM;AACnB,OAAI,aAAa,KAAA,KAAa,YAAY,SACxC,OAAM,IAAI,eAAe,SAAS;AAEpC,OAAI,sBAAsB,KAAA,KAAa,YAAY,kBACjD,OAAM,IAAI,aAAa,mBAAmB,UAAU;AAEtD,UAAO,OAAO,MAAM;AAEpB,OAAI,CADgB,OAAO,MAAM,MAAM,CAErC,OAAM,IAAI,SAAe,SAAS,WAAW;IAC3C,MAAM,gBAAgB;AACpB,YAAO,IAAI,SAAS,QAAQ;AAC5B,cAAS;;IAEX,MAAM,WAAW,QAAe;AAC9B,YAAO,IAAI,SAAS,QAAQ;AAC5B,YAAO,IAAI;;AAEb,WAAO,KAAK,SAAS,QAAQ;AAC7B,WAAO,KAAK,SAAS,QAAQ;KAC7B;;AAGN,UAAQ,gBAAgB;UACjB,KAAK;AACZ,gBAAc,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC;WACzD;AACR,UAAQ,oBAAoB,SAAS,QAAQ;AAC7C,SAAO,aAAa;;CAGtB,IAAI;AACJ,KAAI;AACF,QAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,UAAO,KAAK,QAAuB;AACjC,QAAI,IAAK,QAAO,IAAI;QACf,UAAS;KACd;AACF,UAAO,KAAK,SAAS,OAAO;IAC5B;UACK,KAAK;AACZ,aAAW,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC;;AAGhE,KAAI,aAAa;AACf,QAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;AACnC,QAAM;;AAER,KAAI,UAAU;AACZ,QAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;AACnC,QAAM;;AAGR,KAAI,sBAAsB,KAAA,KAAa,cAAc,mBAAmB;AACtE,QAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;AACnC,QAAM,IAAI,aAAa,mBAAmB,UAAU;;AAGtD,QAAO;EACL;EACA,MAAM,OAAO,OAAO,MAAM;EAC1B;EACD;;;;ACxQH,SAASA,cAAY,KAAsC;AACzD,QAAO;EACL,MAAM,IAAI;EACV,UAAU,IAAI;EACd,UAAU,IAAI;EACd,WAAW,OAAO,IAAI,WAAW;EACjC,WAAW,IAAI;EACf,QAAQ,IAAI;EACZ,QAAQ,IAAI;EACZ,cAAc,IAAI;EAClB,mBAAmB,IAAI;EACvB,cAAc;EACf;;AAGH,SAAS,sBACP,QACA,SAC4B;CAC5B,IAAI,UAAU;CACd,MAAM,kBAAkB;AACtB,MAAI,CAAC,SAAS;AACZ,aAAU;AACV,YAAS;;;CAIb,MAAM,SAAS,OAAO,WAAW;AACjC,QAAO,IAAI,eAA2B;EACpC,MAAM,KAAK,YAAY;AACrB,OAAI;IACF,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,MAAM;AACR,gBAAW;AACX,gBAAW,OAAO;UAElB,YAAW,QAAQ,MAAM;YAEpB,KAAK;AACZ,eAAW;AACX,eAAW,MAAM,IAAI;;;EAGzB,SAAS;AACP,cAAW;AACX,UAAO,QAAQ,CAAC,YAAY,GAAG;;EAElC,CAAC;;AAGJ,IAAa,wBAAb,MAA+D;CAC7D,gCAAiC,IAAI,KAAqB;CAE1D,YACE,IACA,WACA,UACA;AAHiB,OAAA,KAAA;AACA,OAAA,YAAA;AACA,OAAA,WAAA;;CAGnB,MAAM,KAAK,MAAiD;EAC1D,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,WAAW,CACX,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,IACF,QAAOA,cAAY,IAAI;EAGzB,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EACpC,MAAM,UAAU,MAAM,KAAK,uBAAuB,MAAM,IAAI;AAE5D,MAAI,QACF,QAAO;GACL;GACA,UAAU,QAAQ;GAClB,UAAU,QAAQ;GAClB,WAAW,OAAO,QAAQ,WAAW;GACrC,WAAW,QAAQ;GACnB,QAAQ;GACR,QAAQ;GACR,cAAc,QAAQ;GACtB,mBAAmB,QAAQ;GAC3B,cAAc,QAAQ;GACvB;AAGH,QAAM,IAAI,mBAAmB,KAAK;;CAGpC,MAAM,IAAI,MAAwC;AAOhD,UANY,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,OAAO,SAAS,CAChB,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB,GAET,WAAW;;CAGzB,MAAM,IACJ,MACA,QAC6B;EAC7B,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,WAAW,CACX,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,KAAK;AACP,OAAI,IAAI,WAAW,WAAW;IAC5B,MAAM,SAAS,MAAM,KAAK,UAAU,MAAM,MAAM,OAAO;AACvD,QAAI,OAAO,SAAS,QAAQ;AAC1B,WAAM,KAAK,IAAI,MAAM,OAAO,SAAS,UAAU,OAAO,SAAS,KAAK;AACpE,YAAO,KAAK,IAAI,MAAM,OAAO;;AAE/B,QAAI,OAAO,SAAS,UAClB,OAAM,IAAI,kBAAkB,MAAM,OAAO,aAAa;AAExD,UAAM,IAAI,mBAAmB,KAAK;;GAGpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AACpC,SAAM,KAAK,GACR,YAAY,aAAa,CACzB,IAAI,EAAE,sBAAsB,KAAK,CAAC,CAClC,MAAM,QAAQ,KAAK,KAAK,CACxB,SAAS;GAEZ,MAAM,SAASA,cAAY,IAAI;AAC/B,UAAO,oBAAoB;AAE3B,QAAK,cAAc,KAAK;AAQxB,UAAO;IAAE;IAAQ,MAJJ,sBADK,qBADD,KAAK,KAAK,UAAU,IAAI,aAAa,CACN,QAE9C,KAAK,cAAc,KAAK,CACzB;IAEsB;;EAGzB,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EACpC,MAAM,UAAU,MAAM,KAAK,uBAAuB,MAAM,IAAI;AAE5D,MAAI,QACF,OAAM,IAAI,kBAAkB,MAAM,QAAQ,gBAAgB;GACxD,UAAU,QAAQ;GAClB,UAAU,QAAQ;GAClB,WAAW,QAAQ;GACpB,CAAC;EAGJ,MAAM,SAAS,MAAM,KAAK,UAAU,MAAM,MAAM,OAAO;AACvD,MAAI,OAAO,SAAS,QAAQ;AAC1B,SAAM,KAAK,IAAI,MAAM,OAAO,SAAS,UAAU,OAAO,SAAS,KAAK;AACpE,UAAO,KAAK,IAAI,MAAM,OAAO;;AAE/B,MAAI,OAAO,SAAS,UAClB,OAAM,IAAI,kBAAkB,MAAM,OAAO,aAAa;AAExD,QAAM,IAAI,mBAAmB,KAAK;;CAGpC,MAAM,IACJ,MACA,UACA,MACe;EACf,MAAM,WAAW,MAAM,KAAK,GACzB,WAAW,aAAa,CACxB,OAAO,CAAC,QAAQ,SAAS,CAAC,CAC1B,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,UAAU,WAAW,aAAa;AACpC,SAAM,KAAK,QAAQ;AACnB;;EAGF,MAAM,UAAU,oBAAoB,KAAK;AAEzC,QAAM,qBADW,KAAK,KAAK,UAAU,QAAQ,EACR,KAAK;EAE1C,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAEpC,MAAI,CAAC,SACH,OAAM,KAAK,GACR,WAAW,aAAa,CACxB,OAAO;GACN;GACA,WAAW,SAAS;GACpB,WAAW,SAAS;GACpB,YAAY,SAAS;GACrB,WAAW,SAAS,aAAa;GACjC,QAAQ;GACR,cAAc;GACd,QAAQ;GACR,gBAAgB,SAAS;GACzB,sBAAsB;GACvB,CAAC,CACD,YAAY,OAAO,GAAG,OAAO,OAAO,CAAC,WAAW,CAAC,CACjD,SAAS;MAEZ,OAAM,KAAK,GACR,YAAY,aAAa,CACzB,IAAI;GACH,QAAQ;GACR,cAAc;GACd,sBAAsB;GACvB,CAAC,CACD,MAAM,QAAQ,KAAK,KAAK,CACxB,MAAM,UAAU,KAAK,UAAU,CAC/B,SAAS;;CAIhB,MAAM,MAAM,MAAqC;AAC/C,MAAI,KAAK,iBAAiB,KAAK,CAC7B;EAGF,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,OAAO,CAAC,gBAAgB,SAAS,CAAC,CAClC,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,MAAI,CAAC,OAAO,IAAI,WAAW,UACzB;AAIF,QAAM,sBADW,KAAK,KAAK,UAAU,IAAI,aAAa,CACjB;AAErC,QAAM,KAAK,GACR,YAAY,aAAa,CACzB,IAAI,EAAE,QAAQ,WAAW,CAAC,CAC1B,MAAM,QAAQ,KAAK,KAAK,CACxB,SAAS;;CAGd,MAAM,cAA+B;EACnC,MAAM,SAAS,MAAM,KAAK,GACvB,WAAW,aAAa,CACxB,OAAO,GAAW,+BAA+B,GAAG,QAAQ,CAAC,CAC7D,MAAM,UAAU,KAAK,YAAY,CACjC,kBAAkB;AAErB,SAAO,OAAO,QAAQ,SAAS,EAAE;;CAKnC,MAAc,uBACZ,MACA,KAQQ;EACR,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,8BAA8B,CACzC,SAAS,mBAAmB,UAAU,gBAAgB,CACtD,OAAO;GACN;GACA;GACA;GACA;GACA;GACA;GACD,CAAC,CACD,MAAM,iBAAiB,KAAK,KAAK,CACjC,MAAM,oBAAoB,MAAM,KAAK,CACrC,MAAM,oBAAoB,KAAK,IAAI,CACnC,MAAM,gBAAgB,UAAU,KAAK,CACrC,MAAM,UAAU,MAAM,KAAK,CAC3B,QAAQ,oBAAoB,OAAO,CACnC,kBAAkB;AAErB,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO;GACL,WAAW,IAAI;GACf,WAAW,IAAI;GACf,WAAW,IAAI;GACf,YAAY,OAAO,IAAI,WAAW;GAClC,gBAAgB,IAAI;GACpB,gBAAgB,IAAI;GACrB;;CAGH,cAAsB,MAAoB;AACxC,OAAK,cAAc,IAAI,OAAO,KAAK,cAAc,IAAI,KAAK,IAAI,KAAK,EAAE;;CAGvE,cAAsB,MAAoB;EACxC,MAAM,SAAS,KAAK,cAAc,IAAI,KAAK,IAAI,KAAK;AACpD,MAAI,SAAS,EACX,MAAK,cAAc,OAAO,KAAK;MAE/B,MAAK,cAAc,IAAI,MAAM,MAAM;;CAIvC,iBAAyB,MAAuB;AAC9C,UAAQ,KAAK,cAAc,IAAI,KAAK,IAAI,KAAK;;;;;ACxUjD,MAAa,6BAA6B,OAAU,KAAK;AAEzD,SAAS,iBAAiB,KAAkC;AAC1D,QAAO;EACL,eAAe,IAAI;EACnB,UAAU,IAAI;EACd,UAAU,IAAI;EACd,WAAW,IAAI;EACf,cAAc,IAAI;EAClB,cAAc,IAAI;EAClB,YAAY,IAAI;EAChB,WAAW,IAAI,eAAe,OAAO,OAAO,IAAI,WAAW,GAAG;EAC/D;;AAGH,IAAa,yBAAb,MAAiE;CAC/D;CAEA,YACE,IACA,QAAgB,4BAChB;AAFiB,OAAA,KAAA;AAGjB,OAAK,QAAQ;;CAGf,MAAM,OAAO,SAAyD;EACpE,MAAM,gBAAgB,YAAY;EAClC,MAAM,QAAQ,KAAK,KAAK;EACxB,MAAM,MAAM,IAAI,KAAK,MAAM,CAAC,aAAa;EACzC,MAAM,YAAY,IAAI,KAAK,QAAQ,KAAK,MAAM,CAAC,aAAa;AAiB5D,SAAO,iBAfK,MAAM,KAAK,GACpB,WAAW,yBAAyB,CACpC,OAAO;GACN,gBAAgB;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,WAAW,QAAQ,aAAa;GAChC,gBAAgB;GAChB,gBAAgB;GAChB,aAAa,QAAQ,cAAc;GACnC,YAAY,QAAQ,aAAa;GAClC,CAAC,CACD,cAAc,CACd,yBAAyB,CAEA;;CAG9B,MAAM,IAAI,eAA6C;EACrD,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,yBAAyB,CACpC,WAAW,CACX,MAAM,kBAAkB,KAAK,cAAc,CAC3C,MAAM,kBAAkB,MAAM,KAAK,CACnC,kBAAkB;AAErB,MAAI,CAAC,IACH,OAAM,IAAI,oBAAoB,cAAc;AAG9C,SAAO,iBAAiB,IAAI;;CAG9B,MAAM,OAAO,eAAsC;AACjD,QAAM,KAAK,GACR,YAAY,yBAAyB,CACrC,IAAI,EAAE,iCAAgB,IAAI,MAAM,EAAC,aAAa,EAAE,CAAC,CACjD,MAAM,kBAAkB,KAAK,cAAc,CAC3C,MAAM,kBAAkB,MAAM,KAAK,CACnC,SAAS;;CAGd,MAAM,cAAc,sBAAY,IAAI,MAAM,EAAmB;EAC3D,MAAM,SAAS,IAAI,aAAa;EAChC,MAAM,SAAS,MAAM,KAAK,GACvB,YAAY,yBAAyB,CACrC,IAAI,EAAE,gBAAgB,QAAQ,CAAC,CAC/B,MAAM,kBAAkB,MAAM,OAAO,CACrC,MAAM,kBAAkB,MAAM,KAAK,CACnC,kBAAkB;AAErB,SAAO,OAAO,OAAO,eAAe;;;;;;;;;ACvFxC,eAAsBC,KAAG,IAAgC;AACvD,OAAM,GAAG,OACN,YAAY,aAAa,CACzB,UAAU,QAAQ,SAAS,QAAQ,IAAI,YAAY,CAAC,CACpD,UAAU,aAAa,SAAS,QAAQ,IAAI,SAAS,CAAC,CACtD,UAAU,aAAa,SAAS,QAAQ,IAAI,SAAS,CAAC,CACtD,UAAU,cAAc,WAAW,QAAQ,IAAI,SAAS,CAAC,CACzD,UAAU,aAAa,OAAO,CAC9B,UAAU,UAAU,SAAS,QAAQ,IAAI,SAAS,CAAC,UAAU,YAAY,CAAC,CAC1E,UAAU,gBAAgB,SAAS,QAAQ,IAAI,SAAS,CAAC,CACzD,UAAU,UAAU,SAAS,QAAQ,IAAI,SAAS,CAAC,UAAU,QAAQ,CAAC,CACtE,UAAU,kBAAkB,SAAS,QAAQ,IAAI,SAAS,CAAC,CAC3D,UAAU,wBAAwB,SAAS,QAAQ,IAAI,SAAS,CAAC,CACjE,SAAS;AAEZ,OAAM,GAAG,OACN,YAAY,wBAAwB,CACpC,GAAG,aAAa,CAChB,OAAO,SAAS,CAChB,SAAS;AAKZ,OAAM,GAAG,OACN,YAAY,qBAAqB,CACjC,GAAG,aAAa,CAChB,QAAQ,CAAC,UAAU,uBAAuB,CAAC,CAC3C,SAAS;;AAGd,eAAsBC,OAAK,IAAgC;AACzD,OAAM,GAAG,OAAO,UAAU,aAAa,CAAC,UAAU,CAAC,SAAS;;;;;;;;AChC9D,eAAsBC,KAAG,IAAgC;AACvD,OAAM,GAAG,OACN,YAAY,yBAAyB,CACrC,UAAU,kBAAkB,SAAS,QAAQ,IAAI,YAAY,CAAC,CAC9D,UAAU,aAAa,SAAS,QAAQ,IAAI,SAAS,CAAC,CACtD,UAAU,aAAa,SAAS,QAAQ,IAAI,SAAS,CAAC,CACtD,UAAU,aAAa,OAAO,CAC9B,UAAU,kBAAkB,SAAS,QAAQ,IAAI,SAAS,CAAC,CAC3D,SAAS;;AAGd,eAAsBC,OAAK,IAAgC;AACzD,OAAM,GAAG,OAAO,UAAU,yBAAyB,CAAC,UAAU,CAAC,SAAS;;;;;;;;ACZ1E,eAAsBC,KAAG,IAAgC;AACvD,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,UAAU,kBAAkB,OAAO,CACnC,SAAS;AAEZ,OAAM,GACH,YAAY,yBAAyB,CACrC,IAAI,EAAE,gBAAgB,GAAG,kBAAkB,CAAC,CAC5C,MAAM,kBAAkB,MAAM,KAAK,CACnC,SAAS;AAEZ,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,YAAY,mBAAmB,QAAQ,IAAI,YAAY,CAAC,CACxD,SAAS;AAEZ,OAAM,GAAG,OACN,YAAY,6BAA6B,CACzC,GAAG,yBAAyB,CAC5B,OAAO,iBAAiB,CACxB,SAAS;;AAGd,eAAsBC,OAAK,IAAgC;AACzD,OAAM,GAAG,OAAO,UAAU,6BAA6B,CAAC,UAAU,CAAC,SAAS;AAE5E,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,WAAW,iBAAiB,CAC5B,SAAS;;;;;;;;AC9Bd,eAAsBC,KAAG,IAAgC;AACvD,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,UAAU,kBAAkB,OAAO,CACnC,SAAS;;AAGd,eAAsBC,OAAK,IAAgC;AACzD,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,WAAW,iBAAiB,CAC5B,SAAS;;;;;;;;ACXd,eAAsBC,KAAG,IAAgC;AACvD,OAAM,GAAG,OAAO,UAAU,6BAA6B,CAAC,UAAU,CAAC,SAAS;AAE5E,OAAM,GAAG,OACN,YAAY,oCAAoC,CAChD,GAAG,yBAAyB,CAC5B,OAAO,iBAAiB,CACxB,MAAM,GAAY,yBAAyB,CAC3C,SAAS;;AAGd,eAAsBC,OAAK,IAAgC;AACzD,OAAM,GAAG,OACN,UAAU,oCAAoC,CAC9C,UAAU,CACV,SAAS;AAEZ,OAAM,GAAG,OACN,YAAY,6BAA6B,CACzC,GAAG,yBAAyB,CAC5B,OAAO,iBAAiB,CACxB,SAAS;;;;;;;;ACrBd,eAAsBC,KAAG,IAAgC;AACvD,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,UAAU,eAAe,OAAO,CAChC,SAAS;AAEZ,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,UAAU,cAAc,SAAS,CACjC,SAAS;AAMZ,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,mBACC,0CACA,GAAG,gDACJ,CACA,SAAS;AAMZ,OAAM,GAAG,OACN,YAAY,8BAA8B,CAC1C,GAAG,yBAAyB,CAC5B,OAAO,cAAc,CACrB,SAAS;;AAGd,eAAsBC,OAAK,IAAgC;AACzD,OAAM,GAAG,OAAO,UAAU,8BAA8B,CAAC,UAAU,CAAC,SAAS;AAE7E,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,WAAW,aAAa,CACxB,SAAS;AAEZ,OAAM,GAAG,OACN,WAAW,yBAAyB,CACpC,WAAW,cAAc,CACzB,SAAS;;;;ACrCd,MAAa,oBAAoB;AAQjC,MAAMC,eAAa;CACjB,+BAA+BC;CAC/B,gCAAgCC;CAChC,kCAAkCC;CAClC,mCAAmCC;CACnC,oCAAoCC;CACpC,mCAAmCC;CACpC;AAED,IAAMC,kCAAN,MAAiE;CAC/D,gBAAgB;AACd,SAAO,QAAQ,QAAQP,aAAW;;;AAItC,eAAsB,wBACpB,IACA,SAAiB,mBACS;AAC1B,KAAI;AACF,QAAM,GAAG,+BAA+B,IAAI,GAAG,OAAO,GAAG,QAAQ,GAAG;UAC7D,OAAO;AACd,SAAO;GACL,SAAS;GACT,oBAAoB,EAAE;GACtB,OACE,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,0BAA0B;GACxE;;CAGH,MAAM,WAAW,IAAI,SAAS;EAC5B,IAAI,GAAG,WAAW,OAAO;EACzB,UAAU,IAAIO,iCAA+B;EAC7C,sBAAsB;EACvB,CAAC;CAEF,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,MAAM,SAAS,iBAAiB;AAC/C,UAAQ,OAAO;AACf,YAAU,OAAO;UACV,GAAG;AACV,UAAQ;AACR,YAAU,EAAE;;CAGd,MAAM,qBACJ,SAAS,KAAK,WAAW,OAAO,cAAc,IAAI,EAAE;AAEtD,KAAI,MACF,QAAO;EACL,SAAS;EACT;EACA,OACE,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,0BAA0B;EACxE;AAGH,QAAO;EACL,SAAS;EACT;EACD;;;;ACzDH,SAAS,YAAY,KAAsC;AACzD,QAAO;EACL,MAAM,IAAI;EACV,UAAU,IAAI;EACd,UAAU,IAAI;EACd,WAAW,OAAO,IAAI,WAAW;EACjC,WAAW,IAAI;EACf,QAAQ,IAAI;EACZ,QAAQ,IAAI;EACZ,cAAc,IAAI;EAClB,mBAAmB,IAAI;EACvB,cAAc;EACf;;AAGH,IAAa,yBAAb,MAAiE;CAC/D;CACA;CACA;CAEA,YACE,aACA,IACA,UACA,cACA,UACA;AALiB,OAAA,cAAA;AACA,OAAA,KAAA;AACA,OAAA,WAAA;AACA,OAAA,eAAA;AACA,OAAA,WAAA;AAEjB,OAAK,gBAAgB,YAAY;AACjC,OAAK,MACH,YAAY,cAAc,OAAO,UAAU,YAAY,WAAW,GAAG;AACvE,OAAK,eAAe,YAAY;;CAGlC,MAAM,KACJ,MACA,SACiC;AACjC,MACE,KAAK,YAAY,cAAc,QAC/B,KAAK,YAAY,aAAa,KAE9B,OAAM,IAAI,MAAM,2CAA2C;EAO7D,MAAM,oBACJ,KAAK,YAAY,cAAc,OAC1B,KAAK,YAAY,aAAa,KAAA,IAC/B,KAAA;EAGN,MAAM,EAAE,UAAU,MAAM,cAAc,MAAM,mBAC1C,KAAK,UACL,MACA;GACE,UAAU,KAAK;GACf;GACA,GAAI,SAAS,SAAS,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;GACtD,CACF;AAKD,MACE,KAAK,YAAY,cAAc,QAC/B,SAAS,KAAK,YAAY,YAC1B;AACA,SAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;AACnC,SAAM,IAAI,aAAa,KAAK,YAAY,YAAY,KAAK;;AAG3D,MAAI;GACF,MAAM,WAAW,MAAM,KAAK,GACzB,WAAW,aAAa,CACxB,OAAO,CAAC,QAAQ,SAAS,CAAC,CAC1B,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAErB,OAAI,UAAU,WAAW,YAEvB,OAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;QAC9B;IACL,MAAM,UAAU,oBAAoB,KAAK;IACzC,MAAM,WAAW,KAAK,KAAK,UAAU,QAAQ;AAC7C,UAAM,MAAM,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;AACnD,UAAM,OAAO,UAAU,SAAS;IAEhC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAEpC,QAAI,CAAC,SACH,OAAM,KAAK,GACR,WAAW,aAAa,CACxB,OAAO;KACN;KACA,WAAW,KAAK,YAAY;KAC5B,WAAW,KAAK,YAAY;KAC5B,YAAY;KACZ,WAAW,KAAK,YAAY,aAAa;KACzC,QAAQ;KACR,cAAc;KACd,QAAQ;KACR,gBAAgB;KAChB,sBAAsB;KACvB,CAAC,CACD,YAAY,OAAO,GAAG,OAAO,OAAO,CAAC,WAAW,CAAC,CACjD,SAAS;QAGZ,OAAM,KAAK,GACR,YAAY,aAAa,CACzB,IAAI;KACH,QAAQ;KACR,cAAc;KACd,QAAQ;KACR,sBAAsB;KACvB,CAAC,CACD,MAAM,QAAQ,KAAK,KAAK,CACxB,MAAM,UAAU,KAAK,UAAU,CAC/B,SAAS;;WAGT,KAAK;AACZ,SAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;AACnC,SAAM;;AAGR,QAAM,KAAK,aAAa,OAAO,KAAK,cAAc;EAElD,MAAM,MAAM,MAAM,KAAK,GACpB,WAAW,aAAa,CACxB,WAAW,CACX,MAAM,QAAQ,KAAK,KAAK,CACxB,yBAAyB;AAE5B,SAAO;GACL;GACA,KAAK,UAAU,KAAK;GACpB,QAAQ,YAAY,IAAI;GACzB;;;;;AC3JL,IAAa,gCAAb,MAA+E;CAC7E,YACE,IACA,UACA,cACA,UACA;AAJiB,OAAA,KAAA;AACA,OAAA,WAAA;AACA,OAAA,eAAA;AACA,OAAA,WAAA;;CAGnB,aAAa,aAA6C;AACxD,SAAO,IAAI,uBACT,aACA,KAAK,IACL,KAAK,UACL,KAAK,cACL,KAAK,SACN;;;;;;;;;;ACDL,IAAa,8BAAb,MAAuE;CACrE,OAAgB;CAEhB,YACE,OACA,QACA;AAFiB,OAAA,QAAA;AACA,OAAA,SAAA;;CAGnB,MAAM,oBACJ,aACiC;EACjC,MAAM,SAAS,4BACb,MAAM,KAAK,OAAO,aAAa,YAAY,CAC5C;AACD,MAAI,OAAO,SAAS,cAClB,OAAM,IAAI,MAAM,gDAAgD;AAElE,SAAO;;CAGT,MAAM,sBACJ,MACmC;EACnC,MAAM,SAAS,8BACb,MAAM,KAAK,OAAO,eAAe,KAAK,CACvC;AACD,MAAI,OAAO,SAAS,cAClB,OAAM,IAAI,MAAM,kDAAkD;AAEpE,SAAO;;CAGT,OAAO,MAAwC;AAC7C,SAAO,KAAK,MAAM,IAAI,KAAK;;CAG7B,MAAM,SAA2C;AAC/C,SAAO;GACL,MAAM,KAAK;GACX,OAAO,OAAO,KAAK,OAAO,aAAa,IAAI;GAC5C;;;;;ACpDL,IAAM,qBAAN,MAAsD;CACpD;CACA;CACA;CACA;CAEA,YAAY,aAA0B;AACpC,OAAK,gBAAgB,YAAY;AACjC,OAAK,MACH,YAAY,eAAe,OACvB,OACA,UAAU,YAAY,WAAW;AACvC,OAAK,eAAe,YAAY;AAChC,OAAK,eAAe,YAAY;;CAMlC,MAAM,KACJ,MACiC;AACjC,QAAM,KAAK,QAAQ,CAAC,YAAY,GAAG;AACnC,QAAM,IAAI,MAAM,qDAAqD;;;AAIzE,IAAa,4BAAb,MAA2E;CACzE,aAAa,aAA6C;AACxD,SAAO,IAAI,mBAAmB,YAAY;;;;;ACZ9C,IAAa,oBAAb,MAA+B;CAC7B,YAA0C,IAAI,yBAAyB;CACvE;CACA;CACA;CACA;CAEA,YACE,IACA,aACA;AAFiB,OAAA,KAAA;AACA,OAAA,cAAA;;CAGnB,cAAc,WAAuC;AACnD,OAAK,YAAY;AACjB,SAAO;;CAGT,kBAAkB,SAAyC;AACzD,OAAK,sBAAsB;AAC3B,SAAO;;CAGT,YAAY,SAAmC;AAC7C,OAAK,UAAU;AACf,SAAO;;CAGT,mBAAmB,UAAwB;AACzC,OAAK,iBAAiB;AACtB,SAAO;;;;;;;;;;CAWT,uBAAuB,YAA0B;AAC/C,OAAK,qBAAqB;AAC1B,SAAO;;CAGT,MAAM,QAAwC;EAC5C,MAAM,SAAS,MAAM,wBAAwB,KAAK,IAAI,kBAAkB;AACxE,MAAI,CAAC,OAAO,WAAW,OAAO,MAC5B,OAAM,OAAO;EAGf,MAAM,WAAW,KAAK,GAAG,WACvB,kBACD;EAED,MAAM,QAAQ,IAAI,sBAChB,UACA,KAAK,WACL,KAAK,YACN;EACD,MAAM,eAAe,IAAI,uBAAuB,SAAS;EAEzD,MAAM,gBACJ,KAAK,wBACJ,KAAK,SAAS,SAAS,OACpB,IAAI,2BAA2B,GAC/B,IAAI,8BACF,UACA,KAAK,aACL,cACA,KAAK,eACN;EAEP,MAAM,UAAU,IAAI,kBAClB,OACA,cACA,eACA,KAAK,QACN;EAED,IAAI;AACJ,MAAI,KAAK,uBAAuB,KAAA,GAAW;GACzC,MAAM,aAAa,KAAK;AACxB,gBAAa,kBAAkB;AAG7B,iBAAa,eAAe,CAAC,YAAY,GAAG;MAC3C,WAAW;AACd,OAAI,OAAO,WAAW,UAAU,WAC9B,YAAW,OAAO;;EAItB,MAAM,gBAAsB;AAC1B,OAAI,eAAe,KAAA,GAAW;AAC5B,kBAAc,WAAW;AACzB,iBAAa,KAAA;;;AAIjB,SAAO;GACL;GACA;GACA;GACA;GACA,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,SAAS,GAAG,EAAE;GACjD;GACD;;;;;AC3GL,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA,GAAG,OAAO,KAAK,kBAA4C;CAC5D,CAAC;AA2CF,SAAS,gBAAgB,SAAkC;AACzD,QAAO,kBAAkB,QAAQ,aAAa,aAAa,QAAQ,QAAQ,YAAY,QAAQ,WAAW;;AAG5G,SAAS,iBAAiB,SAA0B,QAAuB;AACzE,wBAAO,IAAI,MACT,4CAA4C,gBAAgB,QAAQ,CAAC,IAAI,SAC1E;;AAGH,SAAS,gBACP,SACA,MACA,QACO;AACP,wBAAO,IAAI,MACT,oCAAoC,gBAAgB,QAAQ,CAAC,MAAM,KAAK,IAAI,SAC7E;;AAGH,SAAS,wBACP,QACA,SACuB;CACvB,MAAM,UAAU,OAAO,cAAc,OAAO,eAAe,QACxD,kBAAkB,cAAc,YAAY,QAAQ,QACtD;AACD,KAAI,QAAQ,WAAW,EACrB,OAAM,iBACJ,SACA,QAAQ,WAAW,IACf,6CACA,kDACL;AAEH,QAAO,QAAQ;;AAGjB,SAAS,gBACP,eACA,SACmB;AACnB,QAAO,cAAc,QAAQ,SAAS,wBACpC,oBAAoB,WAAW,KAAK,cAAc;AAChD,MAAI,UAAU,WAAW,KAAM,QAAO;GAAE,UAAU;GAAM;GAAW;AACnE,MAAI;AACF,UAAO;IAAE,UAAU,MAAM,UAAU,OAAO;IAAE;IAAW;UACjD;AACN,SAAM,iBAAiB,SAAS,uCAAuC;;GAEzE,CACH;;AAGH,SAAS,gBACP,YACA,SACwB;CACxB,MAAM,UAAU,WAAW,QACxB,EAAE,gBACD,UAAU,SAAS,QACnB,aAAa,UAAU,KAAK,KAAK,QAAQ,WAC5C;AACD,KAAI,QAAQ,SAAS,EACnB,OAAM,iBAAiB,SAAS,wCAAwC;AAK1E,KAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAO,QAAQ;;AAGjB,SAAS,qBACP,eACA,SACe;CACf,MAAM,gBAAgB,MAAM,KAC1B,uBACC,SAAS,UAAU,OACrB;CACD,MAAM,eAAe,OAAO,OAAO,cAAc,MAAM,CAAC,KACrD,UAAU,MAAM,OAClB;CACD,MAAM,mBAAmB,cAAc,QAAQ,SAC5C,wBACC,oBAAoB,WAAW,SAAS,cACtC,UAAU,WAAW,OAAO,EAAE,GAAG,CAAC,UAAU,OAAO,CACpD,CACJ;AAED,KAAI;AAMF,SAAO,eAAe,sBALL,MACf;GAAC,GAAG;GAAe,GAAG;GAAc,GAAG;GAAiB,CACrD,OAAO,QAAQ,CACf,KAAK,OAAO,CAChB,CACoD,CAAC;SAChD;AACN,QAAM,iBAAiB,SAAS,0CAA0C;;;AAM9E,SAAS,sBAAsB,UAAsC;CACnE,MAAM,uBAAO,IAAI,KAAqB;CACtC,MAAM,cAAc,SAAS,YAAY,QAAQ,eAAe;AAC9D,MACE,EAAE,UAAU,eACZ,WAAW,MAAM,UAAU,KAAA,KAC3B,WAAW,SAAS,KAAK,uBAEzB,QAAO;EAET,MAAM,OAAO,WAAW,KAAK;EAC7B,MAAM,UAAU,MAAM,WAAW;EACjC,MAAM,WAAW,KAAK,IAAI,KAAK;AAC/B,MAAI,aAAa,KAAA,GAAW;AAC1B,QAAK,IAAI,MAAM,QAAQ;AACvB,UAAO;;AAET,SAAO,aAAa;GACpB;AACF,QAAO;EAAE,GAAG;EAAU;EAAa;;AAGrC,SAAS,yBACP,aACa;CACb,MAAM,4BAAY,IAAI,KAAa;CACnC,IAAI,UAAU;AAEd,QAAO,SAAS;AACd,YAAU;AACV,OAAK,MAAM,CAAC,MAAM,eAAe,aAAa;AAC5C,OAAI,UAAU,IAAI,KAAK,CAAE;AAUzB,OAT0B,OAAO,OAAO,WAAW,WAAW,CAAC,CAAC,MAC7D,UAAU;IACT,MAAM,WAAW,aAAa,MAAM,KAAK,CAAC;AAC1C,WACE,aAAa,uBACZ,YAAY,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS;KAG1D,EACsB;AACrB,cAAU,IAAI,KAAK;AACnB,cAAU;;;;AAKhB,QAAO;;AAGT,SAAS,iBACP,MACA,aACW;AACX,KAAI,cAAc,KAAK,CACrB,QAAO;EAAE,GAAG,iBAAiB,KAAK,QAAQ,YAAY;EAAE,UAAU;EAAM;AAE1E,KAAI,WAAW,KAAK,CAClB,QAAO;EACL,MAAM,iBAAiB,KAAK,QAAQ,YAAY;EAChD,MAAM;EACN,UAAU;EACX;CAGH,MAAM,WAAW,KAAK;AACtB,KAAI,aAAa,oBACf,QAAO;EAAE,MAAM;EAAc,UAAU;EAAO;CAEhD,MAAM,OAAO,YAAY,IAAI,SAAS;AACtC,KAAI,CAAC,KACH,OAAM,IAAI,MAAM,6CAA6C,WAAW;AAE1E,QAAO;EAAE;EAAM,MAAM;EAAU,UAAU;EAAO;;AAGlD,SAAS,gBACP,UACA,aACmB;CACnB,MAAM,YAAY,yBAAyB,YAAY;AACvD,KAAI,CAAC,UAAU,IAAI,SAAS,CAAE,QAAO;CAErC,MAAM,8BAAc,IAAI,KAAyB;AACjD,MAAK,MAAM,QAAQ,UAAW,aAAY,IAAI,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC;AAEnE,MAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,aAAa,YAAY,IAAI,KAAK;EACxC,MAAM,OAAO,YAAY,IAAI,KAAK;AAClC,MAAI,CAAC,cAAc,CAAC,KAAM;AAC1B,OAAK,MAAM,SAAS,OAAO,OAAO,WAAW,WAAW,CAAC,EAAE;GACzD,MAAM,WAAW,aAAa,MAAM,KAAK,CAAC;AAC1C,OAAI,aAAa,uBAAuB,CAAC,UAAU,IAAI,SAAS,CAC9D;AAEF,QAAK,OAAO,KAAK;IACf,YAAY,MAAM,iBAAiB,KAAA;IACnC,MAAM,MAAM;IACZ,OAAO,iBAAiB,MAAM,MAAM,YAAY;IACjD,CAAC;;;AAIN,QAAO,YAAY,IAAI,SAAS,IAAI;;AAGtC,SAAS,aACP,OACA,WACA,SACA,MACiB;AACjB,KAAI;AACF,MAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,UAAU,CACzD,QAAO;GAAE,SAAS;GAAO,OAAO,KAAA;GAAW;AAE7C,SAAO;GAAE,SAAS;GAAM,OAAO,MAAM;GAAY;SAC3C;AACN,QAAM,gBAAgB,SAAS,MAAM,oCAAoC;;;AAI7E,SAAS,aACP,MACA,OACA,MACA,SACA,MACA,UACA,eACM;AACN,KAAI,UAAU,QAAQ,UAAU,KAAA,GAAW;AACzC,MAAI,KAAK,SACP,OAAM,gBACJ,SACA,MACA,sCACD;AAEH;;AAGF,KAAI,KAAK,SAAS,cAAc;AAC9B,MAAI,OAAO,UAAU,SACnB,OAAM,gBAAgB,SAAS,MAAM,mCAAmC;AAE1E,MAAI;AACF,YAAS,MAAuB;UAC1B;AACN,SAAM,gBAAgB,SAAS,MAAM,iCAAiC;;AAExE,MAAI,CAAC,SAAS,IAAI,MAAM,EAAE;AACxB,YAAS,IAAI,MAAM;AACnB,QAAK,KAAK,MAAuB;;AAEnC;;AAGF,KAAI,KAAK,SAAS,QAAQ;AACxB,MAAI,CAAC,MAAM,QAAQ,MAAM,CACvB,OAAM,gBAAgB,SAAS,MAAM,kBAAkB;AAEzD,OAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,EACjD,cACE,KAAK,MACL,MAAM,QACN,GAAG,KAAK,GAAG,MAAM,IACjB,SACA,MACA,UACA,cACD;AAEH;;AAGF,KAAI,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CACnD,OAAM,gBAAgB,SAAS,MAAM,2BAA2B;AAElE,KAAI,cAAc,IAAI,MAAM,CAC1B,OAAM,gBAAgB,SAAS,MAAM,mCAAmC;AAG1E,eAAc,IAAI,MAAM;AACxB,KAAI;EACF,MAAM,SAAS;AACf,OAAK,MAAM,SAAS,KAAK,KAAK,QAAQ;GACpC,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM;GACnC,MAAM,SAAS,aAAa,QAAQ,MAAM,MAAM,SAAS,UAAU;AACnE,QAAK,CAAC,OAAO,WAAW,OAAO,UAAU,KAAA,MAAc,MAAM,WAC3D;AAEF,gBACE,MAAM,OACN,OAAO,OACP,WACA,SACA,MACA,UACA,cACD;;WAEK;AACR,gBAAc,OAAO,MAAM;;;AAI/B,IAAM,oCAAN,MAA+E;CAC7E,YACE,SACA,UACA;AAFiB,OAAA,UAAA;AACA,OAAA,WAAA;;CAGnB,QAAQ,QAAiC;AACvC,MAAI,OAAO,SAAS,KAAK,QAAQ,WAC/B,OAAM,gBACJ,KAAK,SACL,SACA,qDACD;AAEH,MAAI,CAAC,KAAK,SAAU,QAAO,EAAE;AAC7B,MACE,OAAO,UAAU,QACjB,OAAO,OAAO,UAAU,YACxB,MAAM,QAAQ,OAAO,MAAM,CAE3B,OAAM,gBAAgB,KAAK,SAAS,SAAS,2BAA2B;EAG1E,MAAM,OAAwB,EAAE;EAChC,MAAM,2BAAW,IAAI,KAAa;EAClC,MAAM,gCAAgB,IAAI,SAAiB;AAC3C,gBAAc,IAAI,OAAO,MAAM;AAC/B,MAAI;GACF,MAAM,QAAQ,OAAO;AACrB,QAAK,MAAM,SAAS,KAAK,SAAS,QAAQ;IACxC,MAAM,OAAO,SAAS,MAAM;IAC5B,MAAM,SAAS,aAAa,OAAO,MAAM,MAAM,KAAK,SAAS,KAAK;AAClE,SACG,CAAC,OAAO,WAAW,OAAO,UAAU,KAAA,MACrC,MAAM,WAEN;AAEF,iBACE,MAAM,OACN,OAAO,OACP,MACA,KAAK,SACL,MACA,UACA,cACD;;YAEK;AACR,iBAAc,OAAO,OAAO,MAAM;;AAEpC,SAAO;;;AAIX,SAAS,iBACP,QACA,YAC6B;CAC7B,MAAM,UAA2B;EAC/B;EACA,cAAc,OAAO,cAAc,OAAO;EAC1C,SAAS,OAAO,WAAW;EAC5B;CACD,MAAM,gBAAgB,wBAAwB,QAAQ,QAAQ;CAE9D,MAAM,WAAW,gBADE,gBAAgB,eAAe,QAAQ,EACb,QAAQ;AACrD,KAAI,aAAa,QAAQ,SAAS,UAAU,WAAW,KACrD,QAAO,IAAI,kCAAkC,SAAS,KAAK;CAE7D,MAAM,kBAAkB,qBAAqB,eAAe,QAAQ;CAEpE,MAAM,gBAAgB,SAAS,UAAU;AACzC,KAAI,kBAAkB,KACpB,OAAM,iBAAiB,SAAS,4BAA4B;CAE9D,MAAM,WAAW,GAAG,WAAW,cAAc,CAAC;CAC9C,MAAM,8BAAc,IAAI,KAAqC;AAC7D,MAAK,MAAM,QAAQ,OAAO,OAAO,gBAAgB,YAAY,CAAC,CAC5D,KAAI,kBAAkB,KAAK,IAAI,CAAC,KAAK,KAAK,WAAW,KAAK,CACxD,aAAY,IAAI,KAAK,MAAM,KAAK;AASpC,KAAI,EANoB,SAAS,UAAU,YAAY,QACpD,gBACE,WAAW,SAAS,KAAK,gCACxB,WAAW,SAAS,KAAK,gCAC3B,WAAW,KAAK,UAAU,SAC7B,GACqB,UAAU,CAAC,YAAY,IAAI,SAAS,CACxD,OAAM,iBACJ,SACA,2DAA2D,SAAS,GACrE;AAGH,QAAO,IAAI,kCACT,SACA,gBAAgB,UAAU,YAAY,CACvC;;AAGH,IAAa,2BAAb,MAA2E;CACzE,wBAAyB,IAAI,SAG1B;CAEH,gBACE,QACA,YAC6B;EAC7B,IAAI,cAAc,KAAK,MAAM,IAAI,OAAO;AACxC,MAAI,CAAC,aAAa;AAChB,iCAAc,IAAI,KAAK;AACvB,QAAK,MAAM,IAAI,QAAQ,YAAY;;EAGrC,MAAM,SAAS,YAAY,IAAI,WAAW;AAC1C,MAAI,OAAQ,QAAO;EAEnB,MAAM,WAAW,iBAAiB,QAAQ,WAAW;AACrD,cAAY,IAAI,YAAY,SAAS;AACrC,SAAO;;;;;ACzfX,IAAa,iCAAb,MAEA;CACE,YAAY,IAA0D;AAAzC,OAAA,KAAA;;CAE7B,MAAM,aAAa,YAAoB,KAAsC;AAQ3E,SAPY,MAAM,KAAK,GACpB,WAAW,uBAAuB,CAClC,OAAO,cAAc,CACrB,MAAM,eAAe,KAAK,WAAW,CACrC,MAAM,kBAAkB,KAAK,IAAI,CACjC,kBAAkB,KAEN,KAAA;;CAGjB,MAAM,cACJ,YACe;AACf,MAAI,WAAW,WAAW,EACxB;AAGF,QAAM,KAAK,GACR,WAAW,uBAAuB,CAClC,OACC,WAAW,KAAK,eAAe;GAC7B,aAAa,UAAU;GACvB,gBAAgB,UAAU;GAC1B,iBAAiB,SAAS,UAAU,IAAI,CAAC;GACzC,oBAAoB,UAAU;GAC9B,QAAQ,UAAU;GAClB,OAAO,UAAU;GACjB,oBAAoB,UAAU;GAC9B,iCAAgB,IAAI,MAAM,EAAC,aAAa;GACzC,EAAE,CACJ,CACA,YAAY,OACX,GAAG,QAAQ,CAAC,eAAe,iBAAiB,CAAC,CAAC,WAAW,CAC1D,CACA,SAAS;;;;;;;;;AChDhB,eAAsB,GAAG,IAAgC;AACvD,OAAM,GAAG,OACN,YAAY,uBAAuB,CACnC,UAAU,eAAe,SAAS,QAAQ,IAAI,SAAS,CAAC,CACxD,UAAU,kBAAkB,SAAS,QAAQ,IAAI,SAAS,CAAC,CAC3D,UAAU,mBAAmB,SAAS,QAAQ,IAAI,SAAS,CAAC,CAC5D,UAAU,sBAAsB,SAAS,QAAQ,IAAI,SAAS,CAAC,CAC/D,UAAU,UAAU,SAAS,QAAQ,IAAI,SAAS,CAAC,CACnD,UAAU,SAAS,SAAS,QAAQ,IAAI,SAAS,CAAC,CAClD,UAAU,sBAAsB,YAAY,QAAQ,IAAI,SAAS,CAAC,CAClE,UAAU,kBAAkB,SAAS,QAAQ,IAAI,SAAS,CAAC,CAC3D,oBAAoB,4CAA4C,CAC/D,eACA,iBACD,CAAC,CACD,SAAS;AAEZ,OAAM,GAAG,OACN,YAAY,+BAA+B,CAC3C,GAAG,uBAAuB,CAC1B,OAAO,iBAAiB,CACxB,SAAS;AAEZ,OAAM,GAAG,OACN,YAAY,gCAAgC,CAC5C,GAAG,uBAAuB,CAC1B,OAAO,kBAAkB,CACzB,SAAS;;AAGd,eAAsB,KAAK,IAAgC;AACzD,OAAM,GAAG,OAAO,UAAU,uBAAuB,CAAC,UAAU,CAAC,SAAS;;;;AC7BxE,MAAa,8BAA8B;AAC3C,MAAa,uCACX;AACF,MAAa,4CACX;AAQF,MAAM,aAAa,EACjB,yCAAyCC,gDAC1C;AAED,IAAM,gCAAN,MAAiE;CAC/D,gBAAgB;AACd,SAAO,QAAQ,QAAQ,WAAW;;;AAItC,SAAS,eAAe,IAAqB,QAA0B;AACrE,QAAO,IAAI,SAAS;EAClB,IAAI,GAAG,WAAW,OAAO;EACzB,UAAU,IAAI,+BAA+B;EAC7C,sBAAsB;EACtB,oBAAoB;EACpB,wBAAwB;EACzB,CAAC;;AAGJ,SAAS,SACP,OACA,SAGoC;CACpC,MAAM,qBACJ,SAAS,KAAK,WAAW,OAAO,cAAc,IAAI,EAAE;AACtD,KAAI,MACF,QAAO;EACL,SAAS;EACT;EACA,OACE,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,0BAA0B;EACxE;AAEH,QAAO;EAAE,SAAS;EAAM;EAAoB;;AAG9C,eAAsB,iCACpB,IACA,SAAiB,6BAC4B;AAC7C,KAAI;AACF,QAAM,GAAG,+BAA+B,IAAI,GAAG,OAAO,GAAG,QAAQ,GAAG;UAC7D,OAAO;AACd,SAAO;GACL,SAAS;GACT,oBAAoB,EAAE;GACtB,OACE,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,0BAA0B;GACxE;;AAGH,KAAI;EACF,MAAM,EAAE,OAAO,YAAY,MAAM,eAC/B,IACA,OACD,CAAC,iBAAiB;AACnB,SAAO,SAAS,OAAO,QAAQ;UACxB,OAAO;AACd,SAAO,SAAS,OAAO,EAAE,CAAC;;;AAI9B,eAAsB,qCACpB,IACA,SAAiB,6BAC4B;AAC7C,KAAI;EACF,MAAM,EAAE,OAAO,YAAY,MAAM,eAAe,IAAI,OAAO,CAAC,aAAa;AACzE,SAAO,SAAS,OAAO,QAAQ;UACxB,OAAO;AACd,SAAO,SAAS,OAAO,EAAE,CAAC;;;AAI9B,eAAsB,sCACpB,IACA,SAAiB,6BACjB;AACA,QAAO,MAAM,eAAe,IAAI,OAAO,CAAC,eAAe;;;;ACjFzD,IAAa,kCAAb,MAA6C;CAC3C,YAAY,IAAsC;AAArB,OAAA,KAAA;;CAE7B,MAAM,QAAsD;EAC1D,MAAM,SAAS,MAAM,iCAAiC,KAAK,GAAG;AAC9D,MAAI,CAAC,OAAO,WAAW,OAAO,MAC5B,OAAM,OAAO;AAOf,SAAO,EAAE,OADK,IAAI,+BAHD,KAAK,GAAG,WACvB,4BACD,CACyD,EAC1C;;;;;ACZpB,MAAa,qCACX;AAEF,IAAa,+BAAb,cAAkD,cAAc;CAC9D,gBAAuC,QAAQ,SAAS;CACxD;;;;;;;;;CASA;CACA;CAEA,YACE,IACA,gBACA,YACA,oBACA,uBACA,gBACA,iBACA;AACA,QAAM,IAAI,gBAAgB,YAAY,oBAAoB;GACxD,aAAa;GACb,oBAAoB;GACrB,CAAC;AAPe,OAAA,wBAAA;AACA,OAAA,iBAAA;AACA,OAAA,kBAAA;;CAQnB,gBAAyB,OAA8C;AACrE,SAAO,KAAK,cAAc,KAAK,8BAA8B,MAAM,CAAC;;CAGtE,OAA+B;AAC7B,SAAO,KAAK,QAAQ,YAAY;GAC9B,MAAM,YAAY,MAAM,KAAK,WAAW;AAExC,OAAI,cAAc,KAAA,EAChB,MAAK,cAAc;OAEnB,OAAM,KAAK,iBAAiB;GAG9B,IAAI,OAAO,MAAM,KAAK,eAAe,gBAAgB,KAAK,YAAY;AACtE,UAAO,KAAK,QAAQ,SAAS,GAAG;AAC9B,UAAM,KAAK,8BAA8B,KAAK,QAAQ;AACtD,QAAI,CAAC,KAAK,KAAM;AAChB,WAAO,MAAM,KAAK,MAAM;;IAE1B;;CAGJ,QAAgB,MAA0C;EACxD,MAAM,SAAS,KAAK,cAAc,KAAK,KAAK;AAC5C,OAAK,gBAAgB,OAAO,YAAY,KAAA,EAAU;AAClD,SAAO;;CAGT,MAAyB,iBACvB,OACe;EACf,MAAM,aAAyC,EAAE;AAEjD,OAAK,MAAM,EAAE,WAAW,aAAa,OAAO;AAC1C,OAAI,UAAU,UAAU,KAAA,EAAW;GAEnC,MAAM,SAAS,KAAK,sBAAsB,UAAU,QAAQ,aAAa;GAKzE,MAAM,OAJY,KAAK,eAAe,gBACpC,QACA,UAAU,OAAO,KAClB,CACsB,QAAQ,UAAU,OAAO;AAEhD,QAAK,MAAM,OAAO,KAChB,YAAW,KAAK;IACd,YAAY,QAAQ;IACpB;IACA,aAAa,UAAU;IACvB,QAAQ,QAAQ;IAChB,OAAO,QAAQ;IACf,SAAS,QAAQ;IAClB,CAAC;;AAIN,MAAI,WAAW,SAAS,EACtB,OAAM,KAAK,gBAAgB,cAAc,WAAW;;CAIxD,MAAc,8BACZ,UACe;EACf,IAAI,aAAa,KAAK,cAAc,SAAS;AAC7C,MAAI,WAAW,WAAW,EAAG;EAE7B,MAAM,cAAc,WAAW,WAAW,SAAS,GAAI,QAAQ;AAE/D,MAAI,KAAK,cAAc,WAAW,GAAG,aAAa;GAChD,MAAM,WAAW,MAAM,KAAK,mBAAmB,YAAY;AAC3D,gBAAa,KAAK,cAAc,CAAC,GAAG,UAAU,GAAG,WAAW,CAAC;;EAQ/D,MAAM,aAAa,KAAK,cAAc,WAAW;AACjD,MAAI,aAAa,eAAe,eAAe,KAAK,kBAAkB;AACpE,QAAK,mBAAmB;AACxB,WAAQ,KACN,IAAI,KAAK,OAAO,YAAY,4BAA4B,YAAY,4BACtC,WAAW,YAAY,aAAa,EAAE,aACrE;;EAGH,MAAM,kBAAkB,KAAK;AAC7B,OAAK,mBAAmB;AACxB,MAAI;AACF,SAAM,MAAM,gBAAgB,WAAW;WAChC,OAAO;AACd,QAAK,cAAc;AAEnB,QAAK,kBAAkB,KAAA;AACvB,SAAM;YACE;AACR,QAAK,mBAAmB,KAAA;;AAK1B,MAAI,aAAa,gBACf,MAAK,kBAAkB,KAAA;WACd,aAAa,YACtB,MAAK,kBAAkB,KAAK,IAC1B,KAAK,mBAAmB,YACxB,YACD;;;;;;CAQL,MAAyB,UACvB,KACA,OACe;EACf,MAAM,SAAS,KAAK;AACpB,MAAI,WAAW,KAAA,GAAW;AACxB,SAAM,MAAM,UAAU,KAAK,MAAM;AACjC;;AAGF,OAAK,cAAc;AACnB,QAAM,IACH,YAAY,YAAY,CACxB,IAAI;GACH,aAAa;GACb,wCAAwB,IAAI,MAAM;GACnC,CAAC,CACD,MAAM,eAAe,KAAK,KAAK,OAAO,YAAY,CAClD,SAAS;;;CAId,cAAsB,OAAuC;EAC3D,IAAI,kBAAkB,KAAK,cAAc;AACzC,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,UAAU,KAAK,QAAQ;AAC7B,OAAI,UAAU,gBAAiB;AAC/B,OAAI,UAAU,gBAAiB;AAC/B;;AAEF,SAAO,kBAAkB;;;;;;;;;;;;;;;;;;;;CAqB3B,MAAc,aAA0D;EACtE,MAAM,aAAa,MAAM,KAAK,eAAe,gBAC3C,KAAK,YACN;AACD,MAAI,KAAK,oBAAoB,KAAA,EAC3B,QAAO;AAIT,MAAI,WAAW,QAAQ,IAAI,QAAQ,YAAY,KAAK,cAAc,GAAG;AACnE,QAAK,kBAAkB,KAAA;AACvB,UAAO;;AAGT,SAAO,KAAK,eAAe,gBAAgB,KAAK,gBAAgB;;CAGlE,MAAc,mBACZ,YACiC;EACjC,MAAM,aAAqC,EAAE;EAC7C,IAAI,OAAO,MAAM,KAAK,YAAY;AAElC,WAAS;AACP,QAAK,MAAM,QAAQ,KAAK,QACtB,KAAI,KAAK,QAAQ,WAAW,WAAY,YAAW,KAAK,KAAK;AAE/D,OACE,KAAK,QAAQ,MAAM,EAAE,cAAc,QAAQ,WAAW,WAAW,IACjE,CAAC,KAAK,KAEN;AAEF,UAAO,MAAM,KAAK,MAAM;;AAG1B,SAAO;;CAGT,cAAsB,OAAuD;EAC3E,MAAM,4BAAY,IAAI,KAAmC;AACzD,OAAK,MAAM,QAAQ,MACjB,WAAU,IAAI,KAAK,QAAQ,SAAS,KAAK;AAE3C,SAAO,CAAC,GAAG,UAAU,QAAQ,CAAC,CAAC,MAC5B,MAAM,UAAU,KAAK,QAAQ,UAAU,MAAM,QAAQ,QACvD;;;;;ACvQL,MAAa,+BAA+B;AAC5C,MAAa,gCAAgC;AAC7C,MAAa,kCAAkC;AAC/C,MAAa,6BAA6B;AAoB1C,SAAS,SAAS,KAAkB,MAAsB;CACxD,MAAM,QAAQ,IAAI;AAClB,KAAI,UAAU,KAAA,KAAa,MAAM,MAAM,CAAC,WAAW,EACjD,OAAM,IAAI,MAAM,GAAG,KAAK,oCAAoC;AAE9D,KAAI,MAAM,MAAM,KAAK,MACnB,OAAM,IAAI,MAAM,GAAG,KAAK,+CAA+C;AAEzE,QAAO;;AAGT,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAQ,CAAC;;AAGvE,SAAS,eAAe,UAAwB;AAC9C,QAAO,mBAAmB,IAAI,SAAS,SAAS,aAAa,CAAC;;AAGhE,SAAS,cAAc,OAAuB;CAC5C,MAAM,UACJ;CACF,IAAI;AACJ,KAAI;AACF,aAAW,IAAI,IAAI,MAAM;SACnB;AACN,QAAM,IAAI,MAAM,QAAQ;;CAE1B,MAAM,kBACJ,SAAS,aAAa,YACrB,SAAS,aAAa,WAAW,eAAe,SAAS;AAC5D,KACE,MAAM,MAAM,KAAK,SACjB,CAAC,mBACD,SAAS,aAAa,MACtB,SAAS,aAAa,MACtB,SAAS,WAAW,MACpB,SAAS,SAAS,GAElB,OAAM,IAAI,MAAM,QAAQ;AAE1B,QAAO,SAAS,UAAU,CAAC,QAAQ,OAAO,GAAG;;AAG/C,SAAS,aACP,KACA,MACA,cACS;CACT,MAAM,QAAQ,IAAI;AAClB,KAAI,UAAU,KAAA,EAAW,QAAO;AAChC,KAAI,UAAU,OAAQ,QAAO;AAC7B,KAAI,UAAU,QAAS,QAAO;AAC9B,OAAM,IAAI,MAAM,GAAG,KAAK,+BAA+B;;AAGzD,SAAS,SACP,KACA,MACA,cACQ;CACR,MAAM,QAAQ,IAAI;AAClB,KAAI,UAAU,KAAA,EAAW,QAAO;AAChC,KAAI,CAAC,aAAa,KAAK,MAAM,CAC3B,OAAM,IAAI,MAAM,GAAG,KAAK,6BAA6B;CAEvD,MAAM,UAAU,OAAO,MAAM;AAC7B,KAAI,CAAC,OAAO,cAAc,QAAQ,IAAI,UAAA,OACpC,OAAM,IAAI,MACR,GAAG,KAAK,yBAAyB,6BAClC;AAEH,QAAO;;AAGT,SAAgB,4BAA4B,QAAwB;AAClE,KACE,OAAO,MAAM,KAAK,UAClB,OAAO,WAAW,IAAI,IACtB,OAAO,SAAS,KAAK,CAErB,OAAM,IAAI,MAAM,iCAAiC;CAEnD,MAAM,aAAa,OAAO,QAAQ,QAAQ,GAAG;AAC7C,KAAI,WAAW,WAAW,EACxB,OAAM,IAAI,MAAM,yCAAyC;CAE3D,MAAM,WAAW,WAAW,MAAM,IAAI;AACtC,KACE,SAAS,MACN,YACC,QAAQ,WAAW,KACnB,YAAY,OACZ,YAAY,QACZ,CAAC,+BAA+B,KAAK,QAAQ,CAChD,CAED,OAAM,IAAI,MAAM,kDAAkD;AAEpE,QAAO,SAAS,KAAK,IAAI;;AAG3B,SAAgB,6BACd,MAAmB,QAAQ,KACF;CACzB,MAAM,WAAW,IAAI;AACrB,KAAI,aAAa,KAAA,KAAa,aAAa,aACzC,QAAO,EAAE,MAAM,cAAc;AAE/B,KAAI,aAAa,KACf,OAAM,IAAI,MAAM,wDAAwD;AAG1E,QAAO;EACL,MAAM;EACN,IAAI;GACF,UAAU,cAAc,SAAS,KAAK,4BAA4B,CAAC;GACnE,QAAQ,SAAS,KAAK,0BAA0B;GAChD,QAAQ,SAAS,KAAK,0BAA0B;GAChD,aAAa,SAAS,KAAK,iCAAiC;GAC5D,iBAAiB,SAAS,KAAK,qCAAqC;GACpE,QAAQ,4BACN,IAAI,wBAAA,cACL;GACD,gBAAgB,aACd,KACA,qCACA,MACD;GACD,kBAAkB,SAChB,KACA,uCAAA,IAED;GACD,oBAAoB,SAClB,KACA,yCAAA,IAED;GACF;EACF;;;;AChKH,MAAM,aAAa;AAEnB,SAAS,aAAa,MAAoB;AACxC,KAAI,CAAC,WAAW,KAAK,KAAK,CACxB,OAAM,IAAI,MACR,8DACD;;AAIL,SAAgB,sBAAsB,MAAc,QAAwB;AAC1E,cAAa,KAAK;AAGlB,KACE,OAAO,WAAW,IAAI,IACtB,OAAO,SAAS,IAAI,IACpB,OAAO,SAAS,KAAK,IACrB,OACG,MAAM,IAAI,CACV,MACE,YACC,QAAQ,WAAW,KACnB,YAAY,OACZ,YAAY,QACZ,CAAC,+BAA+B,KAAK,QAAQ,CAChD,CAEH,OAAM,IAAI,MAAM,mDAAmD;AAErE,QAAO,GAAG,OAAO,GAAG,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG;;AAG9D,SAAgB,kBAAkB,MAAsB;AACtD,cAAa,KAAK;AAClB,QAAOC,SAAO,KAAK,MAAM,MAAM,CAAC,SAAS,SAAS;;;;ACPpD,MAAM,oBAAiC,QAAQ,SAAS,qBACtD,aAAa,QAAoB,SAAkB;CACjD,WAAW;CACX,oBAAoB,IAAI,IAAI,CAAC,wBAAwB,CAAC;CACvD,CAAC;AAEJ,IAAa,yBAAb,MAAoC;CAClC;CACA;CACA;CAEA,YACE,QACA,eAA6B,EAAE,EAC/B;AAFS,OAAA,SAAA;AAGT,OAAK,SACH,aAAa,UACb,IAAI,SAAS;GACX,UAAU,OAAO;GACjB,QAAQ,OAAO;GACf,gBAAgB,OAAO;GACvB,aAAa;IACX,aAAa,OAAO;IACpB,iBAAiB,OAAO;IACzB;GACF,CAAC;AACJ,OAAK,UAAU,aAAa,WAAW;AACvC,OAAK,MAAM,aAAa,8BAAc,IAAI,MAAM;;CAGlD,uBAAuB,MAAsB;AAC3C,SAAO,IAAI,kBAAkB;GAC3B,QAAQ,KAAK,OAAO;GACpB,KAAK,sBAAsB,MAAM,KAAK,OAAO,OAAO;GACrD,CAAC;;CAGJ,sBAAsB,MAAc,UAA0B;AAC5D,MACE,SAAS,MAAM,CAAC,WAAW,KAC3B,SAAS,SAAS,KAAK,IACvB,SAAS,SAAS,KAAK,CAEvB,OAAM,IAAI,MACR,6DACD;AAEH,SAAO,IAAI,iBAAiB;GAC1B,QAAQ,KAAK,OAAO;GACpB,KAAK,sBAAsB,MAAM,KAAK,OAAO,OAAO;GACpD,aAAa;GACb,gBAAgB,kBAAkB,KAAK;GACxC,CAAC;;CAGJ,sBAAsB,MAAsB;AAC1C,SAAO,IAAI,iBAAiB;GAC1B,QAAQ,KAAK,OAAO;GACpB,KAAK,sBAAsB,MAAM,KAAK,OAAO,OAAO;GACrD,CAAC;;CAGJ,WAAW,MAAgC;AACzC,SAAO,KAAK,OAAO,KAAK,KAAK,uBAAuB,KAAK,CAAC;;CAG5D,MAAM,mBACJ,MACA,UACA,aAAa,KAAK,OAAO,kBACQ;EACjC,MAAM,WAAW,kBAAkB,KAAK;AAMxC,SAAO;GACL,MAAM;GACN,QAAQ;GACR,KARU,MAAM,KAAK,QACrB,KAAK,QACL,KAAK,sBAAsB,MAAM,SAAS,EAC1C,WACD;GAKC,SAAS;IACP,gBAAgB;IAChB,yBAAyB;IAC1B;GACD,cAAc,IAAI,KAChB,KAAK,KAAK,CAAC,SAAS,GAAG,aAAa,IACrC,CAAC,aAAa;GAChB;;CAGH,MAAM,qBACJ,MACA,aAAa,KAAK,OAAO,oBACU;AAMnC,SAAO;GACL,MAAM;GACN,QAAQ;GACR,KARU,MAAM,KAAK,QACrB,KAAK,QACL,KAAK,sBAAsB,KAAK,EAChC,WACD;GAKC,SAAS,EAAE;GACX,cAAc,IAAI,KAChB,KAAK,KAAK,CAAC,SAAS,GAAG,aAAa,IACrC,CAAC,aAAa;GAChB;;;AAIL,SAAgB,6BACd,QACA,eAA6B,EAAE,EACP;AACxB,QAAO,IAAI,uBAAuB,QAAQ,aAAa;;;;AChIzD,MAAM,uBAAuB,IAAI,OAAO,GAAG;AAC3C,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACD,CAAC;AAgBF,SAAS,iBAAiB,OAAyB;AACjD,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CACxD,MAAM,gBAAgB;AACtB,KAAI,cAAc,WAAW,mBAAmB,IAAK,QAAO;AAC5D,QAAO,CAAC,cAAc,MAAM,cAAc,KAAK,CAAC,MAC7C,SAAS,OAAO,SAAS,YAAY,uBAAuB,IAAI,KAAK,CACvE;;AAGH,SAAS,4BACP,aAIA;AACA,KAAI,YAAY,eAAe,KAC7B,OAAM,IAAI,MAAM,mDAAmD;AAErE,uBAAsB,YAAY,YAAY,aAAa;AAC3D,KACE,YAAY,cAAc,QAC1B,CAAC,OAAO,cAAc,YAAY,UAAU,IAC5C,YAAY,aAAa,EAEzB,OAAM,IAAI,MACR,sEACD;;;AAKL,IAAa,sBAAb,MAA+D;CAC7D,OAAgB;CAChB;CACA;CACA;CACA;CAEA,YACE,IACA,QACA,eAAgD,EAAE,EAClD;AAHiB,OAAA,KAAA;AACR,OAAA,SAAA;AAGT,OAAK,aAAa,IAAI,uBAAuB,QAAQ,aAAa;AAClE,OAAK,MAAM,aAAa,8BAAc,IAAI,MAAM;AAChD,OAAK,mBACH,aAAa,oBAAoB,OAAO;AAC1C,OAAK,qBACH,aAAa,sBAAsB,OAAO;;CAG9C,MAAM,oBACJ,aACiC;AACjC,8BAA4B,YAAY;EACxC,MAAM,OAAO,YAAY;EACzB,MAAM,MAAM,KAAK,KAAK,CAAC,aAAa;EACpC,MAAM,cAAc,sBAAsB,MAAM,KAAK,OAAO,OAAO;AAEnE,MAAI;AACF,SAAM,KAAK,GACR,WAAW,aAAa,CACxB,OAAO;IACN;IACA,WAAW,YAAY;IACvB,WAAW,YAAY;IACvB,YAAY,YAAY;IACxB,WAAW,YAAY;IACvB,QAAQ;IACR,cAAc;IACd,QAAQ;IACR,gBAAgB,YAAY;IAC5B,sBAAsB;IACvB,CAAC,CACD,YAAY,aACX,SAAS,OAAO,OAAO,CAAC,YAAY;IAClC,WAAW,YAAY;IACvB,WAAW,YAAY;IACvB,YAAY,YAAY;IACxB,WAAW,YAAY;IACvB,QAAQ;IACR,cAAc;IACd,sBAAsB;IACvB,CAAC,CACH,CACA,SAAS;UACN;AACN,SAAM,IAAI,MAAM,6CAA6C;;EAG/D,IAAI;AACJ,MAAI;AACF,YAAS,MAAM,KAAK,WAAW,mBAC7B,MACA,YAAY,UACZ,KAAK,iBACN;UACK;AACN,SAAM,IAAI,MAAM,iDAAiD;;AAKnE,SAAO;;CAGT,MAAM,sBACJ,MACA,YACmC;AACnC,MAAI;AACF,UAAO,MAAM,KAAK,WAAW,qBAC3B,MACA,cAAc,KAAK,mBACpB;UACK;AACN,SAAM,IAAI,MAAM,mDAAmD;;;CAIvE,MAAM,OAAO,MAAwC;AAMnD,MAAI,CALa,MAAM,KAAK,GACzB,WAAW,aAAa,CACxB,OAAO,OAAO,CACd,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB,CACN,QAAO;AAEtB,MAAI;AACF,SAAM,KAAK,WAAW,WAAW,KAAK;AACtC,UAAO;WACA,OAAO;AACd,OAAI,iBAAiB,MAAM,CAAE,QAAO;AAIpC,SAAM,IAAI,MAAM,uCAAuC;;;CAI3D,MAAM,SAA2C;AAC/C,MAAI;AACF,SAAM,KAAK,WAAW,WAAW,qBAAqB;AACtD,UAAO;IAAE,MAAM,KAAK;IAAM,OAAO;IAAM;WAChC,OAAO;AACd,UAAO;IACL,MAAM,KAAK;IACX,OAAO,iBAAiB,MAAM;IAC/B;;;;AAKP,SAAgB,0BACd,IACA,QACA,eAAgD,EAAE,EAC7B;AACrB,QAAO,IAAI,oBAAoB,IAAI,QAAQ,aAAa"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/reactor-attachments",
|
|
3
|
-
"version": "6.2.3-dev.
|
|
3
|
+
"version": "6.2.3-dev.4",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@aws-sdk/client-s3": "3.1087.0",
|
|
32
32
|
"@aws-sdk/s3-request-presigner": "3.1087.0",
|
|
33
33
|
"@powerhousedao/document-engineering": "1.40.5",
|
|
34
|
-
"@powerhousedao/reactor": "6.2.3-dev.
|
|
35
|
-
"@powerhousedao/shared": "6.2.3-dev.
|
|
34
|
+
"@powerhousedao/reactor": "6.2.3-dev.4",
|
|
35
|
+
"@powerhousedao/shared": "6.2.3-dev.4",
|
|
36
36
|
"change-case": "5.4.4",
|
|
37
37
|
"graphql": "^16",
|
|
38
38
|
"kysely": "0.28.16"
|