@powerhousedao/reactor-attachments 6.0.0-dev.208 → 6.0.0-dev.210
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 +101 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +380 -86
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -43,14 +43,19 @@ type AttachmentHeader = {
|
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
45
|
* Metadata provided alongside attachment data during sync.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* `createdAtUtc` is the original upload time, propagated from the source
|
|
47
|
+
* so that the receiving store preserves it instead of synthesizing the
|
|
48
|
+
* fetch time. `lastAccessedAtUtc` is intentionally omitted: it is a
|
|
49
|
+
* per-reactor LRU concern that resets on every read.
|
|
50
|
+
* The remaining fields (hash, status, source, lastAccessedAtUtc) are set
|
|
51
|
+
* by the store when it creates the attachment record.
|
|
48
52
|
*/
|
|
49
53
|
type AttachmentMetadata = {
|
|
50
54
|
mimeType: string;
|
|
51
55
|
fileName: string;
|
|
52
56
|
sizeBytes: number;
|
|
53
57
|
extension: string | null;
|
|
58
|
+
createdAtUtc: string;
|
|
54
59
|
};
|
|
55
60
|
/**
|
|
56
61
|
* Options provided when reserving an attachment slot.
|
|
@@ -95,7 +100,8 @@ type AttachmentTransportConfig = {
|
|
|
95
100
|
};
|
|
96
101
|
/**
|
|
97
102
|
* A reservation for an in-progress attachment upload.
|
|
98
|
-
* Created by reserve(), deleted when upload.send() completes
|
|
103
|
+
* Created by reserve(), deleted when upload.send() completes or
|
|
104
|
+
* once expiresAtUtc has passed and a sweep runs.
|
|
99
105
|
*/
|
|
100
106
|
type Reservation = {
|
|
101
107
|
reservationId: string;
|
|
@@ -103,6 +109,7 @@ type Reservation = {
|
|
|
103
109
|
fileName: string;
|
|
104
110
|
extension: string | null;
|
|
105
111
|
createdAtUtc: string;
|
|
112
|
+
expiresAtUtc: string;
|
|
106
113
|
};
|
|
107
114
|
//#endregion
|
|
108
115
|
//#region src/interfaces.d.ts
|
|
@@ -159,12 +166,15 @@ interface IAttachmentUpload {
|
|
|
159
166
|
send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
|
|
160
167
|
}
|
|
161
168
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
169
|
+
* Read-only subset of IAttachmentStore.
|
|
170
|
+
*
|
|
171
|
+
* Adapters that cannot safely support the local-only write/GC surface
|
|
172
|
+
* (remote stores, forwarding caches) implement this narrow interface
|
|
173
|
+
* instead of stub-rejecting unsupported methods. Consumers that only
|
|
174
|
+
* need to query metadata or stream attachment bytes can take this type
|
|
175
|
+
* to make their dependency requirements explicit.
|
|
166
176
|
*/
|
|
167
|
-
interface
|
|
177
|
+
interface IAttachmentReader {
|
|
168
178
|
/**
|
|
169
179
|
* Get attachment metadata without streaming body data.
|
|
170
180
|
* Does NOT update lastAccessedAtUtc -- this is a metadata check,
|
|
@@ -173,12 +183,6 @@ interface IAttachmentStore {
|
|
|
173
183
|
* @throws AttachmentNotFound if the hash is unknown.
|
|
174
184
|
*/
|
|
175
185
|
stat(hash: AttachmentHash): Promise<AttachmentHeader>;
|
|
176
|
-
/**
|
|
177
|
-
* Check whether attachment data is available locally.
|
|
178
|
-
* Returns true if the bytes can be served from this reactor's store
|
|
179
|
-
* without a transport round-trip. Does not trigger a remote fetch.
|
|
180
|
-
*/
|
|
181
|
-
has(hash: AttachmentHash): Promise<boolean>;
|
|
182
186
|
/**
|
|
183
187
|
* Retrieve attachment header and data stream by hash.
|
|
184
188
|
* Updates lastAccessedAtUtc on access.
|
|
@@ -192,6 +196,20 @@ interface IAttachmentStore {
|
|
|
192
196
|
* record exists).
|
|
193
197
|
*/
|
|
194
198
|
get(hash: AttachmentHash, signal?: AbortSignal): Promise<AttachmentResponse>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Reactor-facing interface for managing local attachment data.
|
|
202
|
+
* The IAttachmentTransport calls put when it receives data from a remote.
|
|
203
|
+
* The store notifies its configured transport when new data arrives,
|
|
204
|
+
* forming a bidirectional store-transport pair.
|
|
205
|
+
*/
|
|
206
|
+
interface IAttachmentStore extends IAttachmentReader {
|
|
207
|
+
/**
|
|
208
|
+
* Check whether attachment data is available locally.
|
|
209
|
+
* Returns true if the bytes can be served from this reactor's store
|
|
210
|
+
* without a transport round-trip. Does not trigger a remote fetch.
|
|
211
|
+
*/
|
|
212
|
+
has(hash: AttachmentHash): Promise<boolean>;
|
|
195
213
|
/**
|
|
196
214
|
* Store attachment data received from a remote (during sync or re-fetch).
|
|
197
215
|
* Called by IAttachmentTransport implementations during sync, and
|
|
@@ -281,6 +299,14 @@ interface IReservationStore {
|
|
|
281
299
|
create(options: ReserveAttachmentOptions): Promise<Reservation>;
|
|
282
300
|
get(reservationId: string): Promise<Reservation>;
|
|
283
301
|
delete(reservationId: string): Promise<void>;
|
|
302
|
+
/**
|
|
303
|
+
* Delete reservations whose expires_at_utc is at or before `now`.
|
|
304
|
+
* Returns the number of rows deleted.
|
|
305
|
+
*
|
|
306
|
+
* Reservations are not auto-swept; consumers should call this on a
|
|
307
|
+
* cron / interval to clean up rows left behind by aborted uploads.
|
|
308
|
+
*/
|
|
309
|
+
deleteExpired(now?: Date): Promise<number>;
|
|
284
310
|
}
|
|
285
311
|
/**
|
|
286
312
|
* Factory for creating transport-specific upload handles.
|
|
@@ -296,7 +322,7 @@ declare class AttachmentService implements IAttachmentService {
|
|
|
296
322
|
private readonly store;
|
|
297
323
|
private readonly reservations;
|
|
298
324
|
private readonly uploadFactory;
|
|
299
|
-
constructor(store:
|
|
325
|
+
constructor(store: IAttachmentReader, reservations: IReservationStore, uploadFactory: IAttachmentUploadFactory);
|
|
300
326
|
reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload>;
|
|
301
327
|
stat(ref: AttachmentRef): Promise<AttachmentHeader>;
|
|
302
328
|
get(ref: AttachmentRef, signal?: AbortSignal): Promise<AttachmentResponse>;
|
|
@@ -329,6 +355,7 @@ interface AttachmentReservationTable {
|
|
|
329
355
|
file_name: string;
|
|
330
356
|
extension: string | null;
|
|
331
357
|
created_at_utc: string;
|
|
358
|
+
expires_at_utc: string;
|
|
332
359
|
}
|
|
333
360
|
interface AttachmentDatabase {
|
|
334
361
|
attachment: AttachmentTable;
|
|
@@ -356,10 +383,12 @@ declare class KyselyAttachmentStore implements IAttachmentStore {
|
|
|
356
383
|
//#region src/storage/kysely/reservation-store.d.ts
|
|
357
384
|
declare class KyselyReservationStore implements IReservationStore {
|
|
358
385
|
private readonly db;
|
|
359
|
-
|
|
386
|
+
private readonly ttlMs;
|
|
387
|
+
constructor(db: Kysely<AttachmentDatabase>, ttlMs?: number);
|
|
360
388
|
create(options: ReserveAttachmentOptions): Promise<Reservation>;
|
|
361
389
|
get(reservationId: string): Promise<Reservation>;
|
|
362
390
|
delete(reservationId: string): Promise<void>;
|
|
391
|
+
deleteExpired(now?: Date): Promise<number>;
|
|
363
392
|
}
|
|
364
393
|
//#endregion
|
|
365
394
|
//#region src/storage/migrations/migrator.d.ts
|
|
@@ -377,8 +406,9 @@ declare class DirectAttachmentUpload implements IAttachmentUpload {
|
|
|
377
406
|
private readonly db;
|
|
378
407
|
private readonly basePath;
|
|
379
408
|
private readonly reservations;
|
|
409
|
+
private readonly maxBytes?;
|
|
380
410
|
readonly reservationId: string;
|
|
381
|
-
constructor(reservationId: string, options: ReserveAttachmentOptions, db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore);
|
|
411
|
+
constructor(reservationId: string, options: ReserveAttachmentOptions, db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore, maxBytes?: number | undefined);
|
|
382
412
|
send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
|
|
383
413
|
}
|
|
384
414
|
//#endregion
|
|
@@ -387,7 +417,8 @@ declare class DirectAttachmentUploadFactory implements IAttachmentUploadFactory
|
|
|
387
417
|
private readonly db;
|
|
388
418
|
private readonly basePath;
|
|
389
419
|
private readonly reservations;
|
|
390
|
-
|
|
420
|
+
private readonly maxBytes?;
|
|
421
|
+
constructor(db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore, maxBytes?: number | undefined);
|
|
391
422
|
createUpload(reservationId: string, options: ReserveAttachmentOptions): IAttachmentUpload;
|
|
392
423
|
}
|
|
393
424
|
//#endregion
|
|
@@ -405,10 +436,58 @@ declare class SwitchboardAttachmentTransport implements IAttachmentTransport {
|
|
|
405
436
|
fetch(hash: AttachmentHash, signal?: AbortSignal): Promise<TransportResponse | null>;
|
|
406
437
|
announce(_hash: AttachmentHash): Promise<void>;
|
|
407
438
|
push(hash: AttachmentHash, remote: string, data: ReadableStream<Uint8Array>): Promise<void>;
|
|
408
|
-
private buildHeaders;
|
|
409
439
|
private parseMetadataHeaders;
|
|
410
440
|
}
|
|
411
441
|
//#endregion
|
|
442
|
+
//#region src/switchboard/remote-reservation-store.d.ts
|
|
443
|
+
type SwitchboardClientConfig = {
|
|
444
|
+
remoteUrl: string;
|
|
445
|
+
jwtHandler?: JwtHandler;
|
|
446
|
+
fetchFn?: typeof fetch;
|
|
447
|
+
};
|
|
448
|
+
declare class RemoteReservationStore implements IReservationStore {
|
|
449
|
+
private readonly remoteUrl;
|
|
450
|
+
private readonly jwtHandler?;
|
|
451
|
+
private readonly fetchFn;
|
|
452
|
+
constructor(config: SwitchboardClientConfig);
|
|
453
|
+
create(options: ReserveAttachmentOptions): Promise<Reservation>;
|
|
454
|
+
get(_reservationId: string): Promise<Reservation>;
|
|
455
|
+
delete(_reservationId: string): Promise<void>;
|
|
456
|
+
deleteExpired(_now?: Date): Promise<number>;
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/switchboard/remote-attachment-upload.d.ts
|
|
460
|
+
declare class RemoteAttachmentUpload implements IAttachmentUpload {
|
|
461
|
+
readonly reservationId: string;
|
|
462
|
+
private readonly remoteUrl;
|
|
463
|
+
private readonly jwtHandler?;
|
|
464
|
+
private readonly fetchFn;
|
|
465
|
+
private readonly options;
|
|
466
|
+
constructor(reservationId: string, options: ReserveAttachmentOptions, config: SwitchboardClientConfig);
|
|
467
|
+
send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/switchboard/remote-attachment-upload-factory.d.ts
|
|
471
|
+
declare class RemoteAttachmentUploadFactory implements IAttachmentUploadFactory {
|
|
472
|
+
private readonly config;
|
|
473
|
+
constructor(config: SwitchboardClientConfig);
|
|
474
|
+
createUpload(reservationId: string, options: ReserveAttachmentOptions): IAttachmentUpload;
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/switchboard/remote-attachment-store.d.ts
|
|
478
|
+
declare class RemoteAttachmentStore implements IAttachmentReader {
|
|
479
|
+
private readonly remoteUrl;
|
|
480
|
+
private readonly jwtHandler?;
|
|
481
|
+
private readonly fetchFn;
|
|
482
|
+
constructor(config: SwitchboardClientConfig);
|
|
483
|
+
stat(hash: AttachmentHash): Promise<AttachmentHeader>;
|
|
484
|
+
get(hash: AttachmentHash, signal?: AbortSignal): Promise<AttachmentResponse>;
|
|
485
|
+
private fetchAttachment;
|
|
486
|
+
}
|
|
487
|
+
//#endregion
|
|
488
|
+
//#region src/switchboard/create-remote-attachment-service.d.ts
|
|
489
|
+
declare function createRemoteAttachmentService(config: SwitchboardClientConfig): IAttachmentService;
|
|
490
|
+
//#endregion
|
|
412
491
|
//#region src/null-attachment-transport.d.ts
|
|
413
492
|
/**
|
|
414
493
|
* No-op transport for deployments without remote sync.
|
|
@@ -432,11 +511,13 @@ declare class AttachmentBuilder {
|
|
|
432
511
|
private readonly storagePath;
|
|
433
512
|
private transport;
|
|
434
513
|
private customUploadFactory?;
|
|
514
|
+
private maxUploadBytes?;
|
|
435
515
|
constructor(db: Kysely<any>, storagePath: string);
|
|
436
516
|
withTransport(transport: IAttachmentTransport): this;
|
|
437
517
|
withUploadFactory(factory: IAttachmentUploadFactory): this;
|
|
518
|
+
withMaxUploadBytes(maxBytes: number): this;
|
|
438
519
|
build(): Promise<AttachmentBuildResult>;
|
|
439
520
|
}
|
|
440
521
|
//#endregion
|
|
441
|
-
export { ATTACHMENT_SCHEMA, type AttachmentBuildResult, AttachmentBuilder, type AttachmentDatabase, type AttachmentHeader, type AttachmentMetadata, AttachmentNotFound, type AttachmentResponse, AttachmentService, type AttachmentStatus, type AttachmentTransportConfig, type AttachmentUploadResult, DirectAttachmentUpload, DirectAttachmentUploadFactory, type IAttachmentService, type IAttachmentStore, type IAttachmentTransport, type IAttachmentTransportFactory, type IAttachmentUpload, type IAttachmentUploadFactory, type IReservationStore, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, type ParsedRef, type Reservation, ReservationNotFound, type ReserveAttachmentOptions, SwitchboardAttachmentTransport, type SwitchboardTransportConfig, type TransportResponse, createRef, parseRef, runAttachmentMigrations };
|
|
522
|
+
export { ATTACHMENT_SCHEMA, type AttachmentBuildResult, AttachmentBuilder, type AttachmentDatabase, type AttachmentHeader, type AttachmentMetadata, AttachmentNotFound, type AttachmentResponse, AttachmentService, type AttachmentStatus, type AttachmentTransportConfig, type AttachmentUploadResult, DirectAttachmentUpload, DirectAttachmentUploadFactory, type IAttachmentService, type IAttachmentStore, type IAttachmentTransport, type IAttachmentTransportFactory, type IAttachmentUpload, type IAttachmentUploadFactory, type IReservationStore, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, type ParsedRef, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, type Reservation, ReservationNotFound, type ReserveAttachmentOptions, SwitchboardAttachmentTransport, type SwitchboardClientConfig, type SwitchboardTransportConfig, type TransportResponse, createRef, createRemoteAttachmentService, parseRef, runAttachmentMigrations };
|
|
442
523
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/interfaces.ts","../src/attachment-service.ts","../src/ref.ts","../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/switchboard/switchboard-attachment-transport.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"mappings":";;;;;;;cAGa,kBAAA,SAA2B,KAAA;cAC1B,UAAA;AAAA;;;;cASD,mBAAA,SAA4B,KAAA;cAC3B,aAAA;AAAA;;AADd;;cAUa,oBAAA,SAA6B,KAAA;cAC5B,GAAA;AAAA;;;;;;KCnBF,gBAAA;;;;;KAMA,gBAAA;EACV,IAAA,EAAM,cAAA;EACN,QAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,MAAA,EAAQ,gBAAA;EACR,MAAA;EACA,YAAA;EACA,iBAAA;AAAA;;;ADGF
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/interfaces.ts","../src/attachment-service.ts","../src/ref.ts","../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/switchboard/switchboard-attachment-transport.ts","../src/switchboard/remote-reservation-store.ts","../src/switchboard/remote-attachment-upload.ts","../src/switchboard/remote-attachment-upload-factory.ts","../src/switchboard/remote-attachment-store.ts","../src/switchboard/create-remote-attachment-service.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { Migrator, sql } from "kysely";
|
|
3
|
-
import { mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { mkdir, rename, rm } from "node:fs/promises";
|
|
4
4
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
5
|
-
import { Readable } from "node:stream";
|
|
6
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
7
|
//#region \0rolldown/runtime.js
|
|
8
8
|
var __defProp = Object.defineProperty;
|
|
9
9
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -44,6 +44,18 @@ var InvalidAttachmentRef = class extends Error {
|
|
|
44
44
|
this.name = "InvalidAttachmentRef";
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Thrown when an upload exceeds the configured maximum byte cap.
|
|
49
|
+
* Route handlers should map this to HTTP 413 Payload Too Large.
|
|
50
|
+
*/
|
|
51
|
+
var UploadTooLarge = class extends Error {
|
|
52
|
+
maxBytes;
|
|
53
|
+
constructor(maxBytes) {
|
|
54
|
+
super(`Upload exceeds maximum size of ${maxBytes} bytes`);
|
|
55
|
+
this.name = "UploadTooLarge";
|
|
56
|
+
this.maxBytes = maxBytes;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
47
59
|
//#endregion
|
|
48
60
|
//#region src/ref.ts
|
|
49
61
|
const REF_PATTERN = /^attachment:\/\/v(\d+):(.+)$/;
|
|
@@ -128,13 +140,67 @@ async function deleteAttachmentBytes(path) {
|
|
|
128
140
|
await rm(path, { force: true });
|
|
129
141
|
}
|
|
130
142
|
/**
|
|
131
|
-
*
|
|
143
|
+
* Stream bytes to a temp file under `${basePath}/.tmp/` while computing the
|
|
144
|
+
* SHA-256 hash, returning the temp path, hex hash, and total bytes written.
|
|
145
|
+
*
|
|
146
|
+
* Bytes are never buffered in memory beyond the current chunk. The caller is
|
|
147
|
+
* responsible for renaming the temp file to its final hash-derived location
|
|
148
|
+
* (or removing it if the content is a duplicate).
|
|
149
|
+
*
|
|
150
|
+
* If `maxBytes` is set and the input exceeds it, the temp file is removed and
|
|
151
|
+
* `UploadTooLarge` is thrown.
|
|
132
152
|
*/
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
async function streamHashAndWrite(basePath, data, options = {}) {
|
|
154
|
+
const { maxBytes } = options;
|
|
155
|
+
const tmpDir = join(basePath, ".tmp");
|
|
156
|
+
await mkdir(tmpDir, { recursive: true });
|
|
157
|
+
const tempPath = join(tmpDir, randomUUID());
|
|
158
|
+
const hasher = createHash("sha256");
|
|
159
|
+
const writer = createWriteStream(tempPath);
|
|
160
|
+
const reader = data.getReader();
|
|
161
|
+
let sizeBytes = 0;
|
|
162
|
+
let caughtError;
|
|
163
|
+
try {
|
|
164
|
+
for (;;) {
|
|
165
|
+
const { done, value } = await reader.read();
|
|
166
|
+
if (done) break;
|
|
167
|
+
sizeBytes += value.byteLength;
|
|
168
|
+
if (maxBytes !== void 0 && sizeBytes > maxBytes) throw new UploadTooLarge(maxBytes);
|
|
169
|
+
hasher.update(value);
|
|
170
|
+
if (!writer.write(value)) await new Promise((resolve, reject) => {
|
|
171
|
+
const onDrain = () => {
|
|
172
|
+
writer.off("error", onError);
|
|
173
|
+
resolve();
|
|
174
|
+
};
|
|
175
|
+
const onError = (err) => {
|
|
176
|
+
writer.off("drain", onDrain);
|
|
177
|
+
reject(err);
|
|
178
|
+
};
|
|
179
|
+
writer.once("drain", onDrain);
|
|
180
|
+
writer.once("error", onError);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
caughtError = err instanceof Error ? err : new Error(String(err));
|
|
185
|
+
} finally {
|
|
186
|
+
reader.releaseLock();
|
|
187
|
+
}
|
|
188
|
+
await new Promise((resolve, reject) => {
|
|
189
|
+
writer.end((err) => {
|
|
190
|
+
if (err) reject(err);
|
|
191
|
+
else resolve();
|
|
192
|
+
});
|
|
193
|
+
writer.on("error", reject);
|
|
194
|
+
});
|
|
195
|
+
if (caughtError) {
|
|
196
|
+
await rm(tempPath, { force: true });
|
|
197
|
+
throw caughtError;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
tempPath,
|
|
201
|
+
hash: hasher.digest("hex"),
|
|
202
|
+
sizeBytes
|
|
203
|
+
};
|
|
138
204
|
}
|
|
139
205
|
//#endregion
|
|
140
206
|
//#region src/storage/kysely/attachment-store.ts
|
|
@@ -231,7 +297,7 @@ var KyselyAttachmentStore = class {
|
|
|
231
297
|
status: "available",
|
|
232
298
|
storage_path: relPath,
|
|
233
299
|
source: "sync",
|
|
234
|
-
created_at_utc:
|
|
300
|
+
created_at_utc: metadata.createdAtUtc,
|
|
235
301
|
last_accessed_at_utc: now
|
|
236
302
|
}).onConflict((oc) => oc.column("hash").doNothing()).execute();
|
|
237
303
|
else await this.db.updateTable("attachment").set({
|
|
@@ -265,28 +331,35 @@ var KyselyAttachmentStore = class {
|
|
|
265
331
|
};
|
|
266
332
|
//#endregion
|
|
267
333
|
//#region src/storage/kysely/reservation-store.ts
|
|
334
|
+
const DEFAULT_RESERVATION_TTL_MS = 1440 * 60 * 1e3;
|
|
268
335
|
function rowToReservation(row) {
|
|
269
336
|
return {
|
|
270
337
|
reservationId: row.reservation_id,
|
|
271
338
|
mimeType: row.mime_type,
|
|
272
339
|
fileName: row.file_name,
|
|
273
340
|
extension: row.extension,
|
|
274
|
-
createdAtUtc: row.created_at_utc
|
|
341
|
+
createdAtUtc: row.created_at_utc,
|
|
342
|
+
expiresAtUtc: row.expires_at_utc
|
|
275
343
|
};
|
|
276
344
|
}
|
|
277
345
|
var KyselyReservationStore = class {
|
|
278
|
-
|
|
346
|
+
ttlMs;
|
|
347
|
+
constructor(db, ttlMs = DEFAULT_RESERVATION_TTL_MS) {
|
|
279
348
|
this.db = db;
|
|
349
|
+
this.ttlMs = ttlMs;
|
|
280
350
|
}
|
|
281
351
|
async create(options) {
|
|
282
352
|
const reservationId = randomUUID();
|
|
283
|
-
const
|
|
353
|
+
const nowMs = Date.now();
|
|
354
|
+
const now = new Date(nowMs).toISOString();
|
|
355
|
+
const expiresAt = new Date(nowMs + this.ttlMs).toISOString();
|
|
284
356
|
return rowToReservation(await this.db.insertInto("attachment_reservation").values({
|
|
285
357
|
reservation_id: reservationId,
|
|
286
358
|
mime_type: options.mimeType,
|
|
287
359
|
file_name: options.fileName,
|
|
288
360
|
extension: options.extension ?? null,
|
|
289
|
-
created_at_utc: now
|
|
361
|
+
created_at_utc: now,
|
|
362
|
+
expires_at_utc: expiresAt
|
|
290
363
|
}).returningAll().executeTakeFirstOrThrow());
|
|
291
364
|
}
|
|
292
365
|
async get(reservationId) {
|
|
@@ -297,39 +370,60 @@ var KyselyReservationStore = class {
|
|
|
297
370
|
async delete(reservationId) {
|
|
298
371
|
await this.db.deleteFrom("attachment_reservation").where("reservation_id", "=", reservationId).execute();
|
|
299
372
|
}
|
|
373
|
+
async deleteExpired(now = /* @__PURE__ */ new Date()) {
|
|
374
|
+
const result = await this.db.deleteFrom("attachment_reservation").where("expires_at_utc", "<=", now.toISOString()).executeTakeFirst();
|
|
375
|
+
return Number(result.numDeletedRows ?? 0);
|
|
376
|
+
}
|
|
300
377
|
};
|
|
301
378
|
//#endregion
|
|
302
379
|
//#region src/storage/migrations/001_create_attachment_table.ts
|
|
303
380
|
var _001_create_attachment_table_exports = /* @__PURE__ */ __exportAll({
|
|
304
|
-
down: () => down$
|
|
305
|
-
up: () => up$
|
|
381
|
+
down: () => down$2,
|
|
382
|
+
up: () => up$2
|
|
306
383
|
});
|
|
307
|
-
async function up$
|
|
384
|
+
async function up$2(db) {
|
|
308
385
|
await db.schema.createTable("attachment").addColumn("hash", "text", (col) => col.primaryKey()).addColumn("mime_type", "text", (col) => col.notNull()).addColumn("file_name", "text", (col) => col.notNull()).addColumn("size_bytes", "bigint", (col) => col.notNull()).addColumn("extension", "text").addColumn("status", "text", (col) => col.notNull().defaultTo("available")).addColumn("storage_path", "text", (col) => col.notNull()).addColumn("source", "text", (col) => col.notNull().defaultTo("local")).addColumn("created_at_utc", "text", (col) => col.notNull()).addColumn("last_accessed_at_utc", "text", (col) => col.notNull()).execute();
|
|
309
386
|
await db.schema.createIndex("idx_attachment_status").on("attachment").column("status").execute();
|
|
310
387
|
await db.schema.createIndex("idx_attachment_lru").on("attachment").columns(["status", "last_accessed_at_utc"]).execute();
|
|
311
388
|
}
|
|
312
|
-
async function down$
|
|
389
|
+
async function down$2(db) {
|
|
313
390
|
await db.schema.dropTable("attachment").ifExists().execute();
|
|
314
391
|
}
|
|
315
392
|
//#endregion
|
|
316
393
|
//#region src/storage/migrations/002_create_reservation_table.ts
|
|
317
394
|
var _002_create_reservation_table_exports = /* @__PURE__ */ __exportAll({
|
|
395
|
+
down: () => down$1,
|
|
396
|
+
up: () => up$1
|
|
397
|
+
});
|
|
398
|
+
async function up$1(db) {
|
|
399
|
+
await db.schema.createTable("attachment_reservation").addColumn("reservation_id", "text", (col) => col.primaryKey()).addColumn("mime_type", "text", (col) => col.notNull()).addColumn("file_name", "text", (col) => col.notNull()).addColumn("extension", "text").addColumn("created_at_utc", "text", (col) => col.notNull()).execute();
|
|
400
|
+
}
|
|
401
|
+
async function down$1(db) {
|
|
402
|
+
await db.schema.dropTable("attachment_reservation").ifExists().execute();
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/storage/migrations/003_add_reservation_expires_at.ts
|
|
406
|
+
var _003_add_reservation_expires_at_exports = /* @__PURE__ */ __exportAll({
|
|
318
407
|
down: () => down,
|
|
319
408
|
up: () => up
|
|
320
409
|
});
|
|
321
410
|
async function up(db) {
|
|
322
|
-
await db.schema.
|
|
411
|
+
await db.schema.alterTable("attachment_reservation").addColumn("expires_at_utc", "text").execute();
|
|
412
|
+
await db.updateTable("attachment_reservation").set({ expires_at_utc: sql`created_at_utc` }).where("expires_at_utc", "is", null).execute();
|
|
413
|
+
await db.schema.alterTable("attachment_reservation").alterColumn("expires_at_utc", (col) => col.setNotNull()).execute();
|
|
414
|
+
await db.schema.createIndex("idx_reservation_expires_at").on("attachment_reservation").column("expires_at_utc").execute();
|
|
323
415
|
}
|
|
324
416
|
async function down(db) {
|
|
325
|
-
await db.schema.
|
|
417
|
+
await db.schema.dropIndex("idx_reservation_expires_at").ifExists().execute();
|
|
418
|
+
await db.schema.alterTable("attachment_reservation").dropColumn("expires_at_utc").execute();
|
|
326
419
|
}
|
|
327
420
|
//#endregion
|
|
328
421
|
//#region src/storage/migrations/migrator.ts
|
|
329
422
|
const ATTACHMENT_SCHEMA = "attachments";
|
|
330
423
|
const migrations = {
|
|
331
424
|
"001_create_attachment_table": _001_create_attachment_table_exports,
|
|
332
|
-
"002_create_reservation_table": _002_create_reservation_table_exports
|
|
425
|
+
"002_create_reservation_table": _002_create_reservation_table_exports,
|
|
426
|
+
"003_add_reservation_expires_at": _003_add_reservation_expires_at_exports
|
|
333
427
|
};
|
|
334
428
|
var ProgrammaticMigrationProvider = class {
|
|
335
429
|
getMigrations() {
|
|
@@ -387,62 +481,49 @@ function rowToHeader(row) {
|
|
|
387
481
|
lastAccessedAtUtc: row.last_accessed_at_utc
|
|
388
482
|
};
|
|
389
483
|
}
|
|
390
|
-
async function collectAndHash(data) {
|
|
391
|
-
const hasher = createHash("sha256");
|
|
392
|
-
const chunks = [];
|
|
393
|
-
const reader = data.getReader();
|
|
394
|
-
for (;;) {
|
|
395
|
-
const { done, value } = await reader.read();
|
|
396
|
-
if (done) break;
|
|
397
|
-
hasher.update(value);
|
|
398
|
-
chunks.push(value);
|
|
399
|
-
}
|
|
400
|
-
const totalLength = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
|
401
|
-
const bytes = new Uint8Array(totalLength);
|
|
402
|
-
let offset = 0;
|
|
403
|
-
for (const chunk of chunks) {
|
|
404
|
-
bytes.set(chunk, offset);
|
|
405
|
-
offset += chunk.byteLength;
|
|
406
|
-
}
|
|
407
|
-
return {
|
|
408
|
-
bytes,
|
|
409
|
-
hash: hasher.digest("hex")
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
484
|
var DirectAttachmentUpload = class {
|
|
413
485
|
reservationId;
|
|
414
|
-
constructor(reservationId, options, db, basePath, reservations) {
|
|
486
|
+
constructor(reservationId, options, db, basePath, reservations, maxBytes) {
|
|
415
487
|
this.options = options;
|
|
416
488
|
this.db = db;
|
|
417
489
|
this.basePath = basePath;
|
|
418
490
|
this.reservations = reservations;
|
|
491
|
+
this.maxBytes = maxBytes;
|
|
419
492
|
this.reservationId = reservationId;
|
|
420
493
|
}
|
|
421
494
|
async send(data) {
|
|
422
|
-
const {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
495
|
+
const { tempPath, hash, sizeBytes } = await streamHashAndWrite(this.basePath, data, { maxBytes: this.maxBytes });
|
|
496
|
+
try {
|
|
497
|
+
const existing = await this.db.selectFrom("attachment").select(["hash", "status"]).where("hash", "=", hash).executeTakeFirst();
|
|
498
|
+
if (existing?.status === "available") await rm(tempPath, { force: true });
|
|
499
|
+
else {
|
|
500
|
+
const relPath = storageRelativePath(hash);
|
|
501
|
+
const fullPath = join(this.basePath, relPath);
|
|
502
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
503
|
+
await rename(tempPath, fullPath);
|
|
504
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
505
|
+
if (!existing) await this.db.insertInto("attachment").values({
|
|
506
|
+
hash,
|
|
507
|
+
mime_type: this.options.mimeType,
|
|
508
|
+
file_name: this.options.fileName,
|
|
509
|
+
size_bytes: sizeBytes,
|
|
510
|
+
extension: this.options.extension ?? null,
|
|
511
|
+
status: "available",
|
|
512
|
+
storage_path: relPath,
|
|
513
|
+
source: "local",
|
|
514
|
+
created_at_utc: now,
|
|
515
|
+
last_accessed_at_utc: now
|
|
516
|
+
}).onConflict((oc) => oc.column("hash").doNothing()).execute();
|
|
517
|
+
else await this.db.updateTable("attachment").set({
|
|
518
|
+
status: "available",
|
|
519
|
+
storage_path: relPath,
|
|
520
|
+
source: "local",
|
|
521
|
+
last_accessed_at_utc: now
|
|
522
|
+
}).where("hash", "=", hash).where("status", "=", "evicted").execute();
|
|
523
|
+
}
|
|
524
|
+
} catch (err) {
|
|
525
|
+
await rm(tempPath, { force: true });
|
|
526
|
+
throw err;
|
|
446
527
|
}
|
|
447
528
|
await this.reservations.delete(this.reservationId);
|
|
448
529
|
const row = await this.db.selectFrom("attachment").selectAll().where("hash", "=", hash).executeTakeFirstOrThrow();
|
|
@@ -456,16 +537,27 @@ var DirectAttachmentUpload = class {
|
|
|
456
537
|
//#endregion
|
|
457
538
|
//#region src/direct/direct-attachment-upload-factory.ts
|
|
458
539
|
var DirectAttachmentUploadFactory = class {
|
|
459
|
-
constructor(db, basePath, reservations) {
|
|
540
|
+
constructor(db, basePath, reservations, maxBytes) {
|
|
460
541
|
this.db = db;
|
|
461
542
|
this.basePath = basePath;
|
|
462
543
|
this.reservations = reservations;
|
|
544
|
+
this.maxBytes = maxBytes;
|
|
463
545
|
}
|
|
464
546
|
createUpload(reservationId, options) {
|
|
465
|
-
return new DirectAttachmentUpload(reservationId, options, this.db, this.basePath, this.reservations);
|
|
547
|
+
return new DirectAttachmentUpload(reservationId, options, this.db, this.basePath, this.reservations, this.maxBytes);
|
|
466
548
|
}
|
|
467
549
|
};
|
|
468
550
|
//#endregion
|
|
551
|
+
//#region src/switchboard/build-auth-headers.ts
|
|
552
|
+
async function buildAuthHeaders(url, jwtHandler) {
|
|
553
|
+
const headers = {};
|
|
554
|
+
if (jwtHandler) {
|
|
555
|
+
const token = await jwtHandler(url);
|
|
556
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
557
|
+
}
|
|
558
|
+
return headers;
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
469
561
|
//#region src/switchboard/switchboard-attachment-transport.ts
|
|
470
562
|
var SwitchboardAttachmentTransport = class {
|
|
471
563
|
remoteUrl;
|
|
@@ -478,7 +570,7 @@ var SwitchboardAttachmentTransport = class {
|
|
|
478
570
|
}
|
|
479
571
|
async fetch(hash, signal) {
|
|
480
572
|
const url = `${this.remoteUrl}/attachments/${hash}`;
|
|
481
|
-
const headers = await this.
|
|
573
|
+
const headers = await buildAuthHeaders(url, this.jwtHandler);
|
|
482
574
|
const response = await this.fetchFn(url, {
|
|
483
575
|
signal,
|
|
484
576
|
headers
|
|
@@ -497,7 +589,7 @@ var SwitchboardAttachmentTransport = class {
|
|
|
497
589
|
async announce(_hash) {}
|
|
498
590
|
async push(hash, remote, data) {
|
|
499
591
|
const url = `${remote}/attachments/${hash}`;
|
|
500
|
-
const headers = await this.
|
|
592
|
+
const headers = await buildAuthHeaders(url, this.jwtHandler);
|
|
501
593
|
const response = await this.fetchFn(url, {
|
|
502
594
|
method: "PUT",
|
|
503
595
|
body: data,
|
|
@@ -506,26 +598,223 @@ var SwitchboardAttachmentTransport = class {
|
|
|
506
598
|
});
|
|
507
599
|
if (!response.ok) throw new Error(`Attachment push failed: ${response.status} ${response.statusText}`);
|
|
508
600
|
}
|
|
509
|
-
async buildHeaders(url) {
|
|
510
|
-
const headers = {};
|
|
511
|
-
if (this.jwtHandler) {
|
|
512
|
-
const token = await this.jwtHandler(url);
|
|
513
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
514
|
-
}
|
|
515
|
-
return headers;
|
|
516
|
-
}
|
|
517
601
|
parseMetadataHeaders(response) {
|
|
518
602
|
const metaHeader = response.headers.get("X-Attachment-Metadata");
|
|
519
|
-
if (metaHeader)
|
|
603
|
+
if (metaHeader) try {
|
|
604
|
+
const parsed = JSON.parse(metaHeader);
|
|
605
|
+
if (isRecord$1(parsed) && parsed.extension === void 0) parsed.extension = null;
|
|
606
|
+
if (isAttachmentMetadata$1(parsed)) return parsed;
|
|
607
|
+
} catch {}
|
|
608
|
+
return contentTypeFallback$1(response);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
function isRecord$1(value) {
|
|
612
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
613
|
+
}
|
|
614
|
+
function isAttachmentMetadata$1(value) {
|
|
615
|
+
if (!isRecord$1(value)) return false;
|
|
616
|
+
if (typeof value.mimeType !== "string") return false;
|
|
617
|
+
if (typeof value.fileName !== "string") return false;
|
|
618
|
+
if (typeof value.sizeBytes !== "number" || !Number.isFinite(value.sizeBytes) || value.sizeBytes < 0) return false;
|
|
619
|
+
if (value.extension !== null && typeof value.extension !== "string") return false;
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
function contentTypeFallback$1(response) {
|
|
623
|
+
const contentLength = response.headers.get("Content-Length");
|
|
624
|
+
if (contentLength === null) throw new Error("Switchboard response missing both X-Attachment-Metadata and Content-Length headers");
|
|
625
|
+
const sizeBytes = Number(contentLength);
|
|
626
|
+
if (!Number.isInteger(sizeBytes) || sizeBytes < 0) throw new Error(`Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`);
|
|
627
|
+
const lastModified = response.headers.get("Last-Modified");
|
|
628
|
+
const dateHeader = response.headers.get("Date");
|
|
629
|
+
const createdAtUtc = lastModified ? new Date(lastModified).toISOString() : dateHeader ? new Date(dateHeader).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
630
|
+
return {
|
|
631
|
+
mimeType: response.headers.get("Content-Type") ?? "application/octet-stream",
|
|
632
|
+
fileName: "unknown",
|
|
633
|
+
sizeBytes,
|
|
634
|
+
extension: null,
|
|
635
|
+
createdAtUtc
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
//#endregion
|
|
639
|
+
//#region src/switchboard/remote-reservation-store.ts
|
|
640
|
+
var RemoteReservationStore = class {
|
|
641
|
+
remoteUrl;
|
|
642
|
+
jwtHandler;
|
|
643
|
+
fetchFn;
|
|
644
|
+
constructor(config) {
|
|
645
|
+
this.remoteUrl = config.remoteUrl;
|
|
646
|
+
this.jwtHandler = config.jwtHandler;
|
|
647
|
+
this.fetchFn = config.fetchFn ?? globalThis.fetch;
|
|
648
|
+
}
|
|
649
|
+
async create(options) {
|
|
650
|
+
const url = `${this.remoteUrl}/attachments/reservations`;
|
|
651
|
+
const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
|
|
652
|
+
const response = await this.fetchFn(url, {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers: {
|
|
655
|
+
...authHeaders,
|
|
656
|
+
"Content-Type": "application/json"
|
|
657
|
+
},
|
|
658
|
+
body: JSON.stringify({
|
|
659
|
+
mimeType: options.mimeType,
|
|
660
|
+
fileName: options.fileName,
|
|
661
|
+
extension: options.extension ?? null
|
|
662
|
+
})
|
|
663
|
+
});
|
|
664
|
+
if (!response.ok) throw new Error(`Reservation create failed: ${response.status} ${response.statusText}`);
|
|
665
|
+
const json = await response.json();
|
|
666
|
+
const now = /* @__PURE__ */ new Date();
|
|
667
|
+
return {
|
|
668
|
+
reservationId: json.reservationId,
|
|
669
|
+
mimeType: options.mimeType,
|
|
670
|
+
fileName: options.fileName,
|
|
671
|
+
extension: options.extension ?? null,
|
|
672
|
+
createdAtUtc: json.createdAtUtc ?? now.toISOString(),
|
|
673
|
+
expiresAtUtc: json.expiresAtUtc ?? new Date(now.getTime() + 1440 * 60 * 1e3).toISOString()
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
get(_reservationId) {
|
|
677
|
+
return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.get is not supported"));
|
|
678
|
+
}
|
|
679
|
+
delete(_reservationId) {
|
|
680
|
+
return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.delete is not supported"));
|
|
681
|
+
}
|
|
682
|
+
deleteExpired(_now) {
|
|
683
|
+
return Promise.reject(/* @__PURE__ */ new Error("RemoteReservationStore.deleteExpired is not supported"));
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/switchboard/remote-attachment-upload.ts
|
|
688
|
+
var RemoteAttachmentUpload = class {
|
|
689
|
+
reservationId;
|
|
690
|
+
remoteUrl;
|
|
691
|
+
jwtHandler;
|
|
692
|
+
fetchFn;
|
|
693
|
+
options;
|
|
694
|
+
constructor(reservationId, options, config) {
|
|
695
|
+
this.reservationId = reservationId;
|
|
696
|
+
this.options = options;
|
|
697
|
+
this.remoteUrl = config.remoteUrl;
|
|
698
|
+
this.jwtHandler = config.jwtHandler;
|
|
699
|
+
this.fetchFn = config.fetchFn ?? globalThis.fetch;
|
|
700
|
+
}
|
|
701
|
+
async send(data) {
|
|
702
|
+
const url = `${this.remoteUrl}/attachments/reservations/${this.reservationId}`;
|
|
703
|
+
const authHeaders = await buildAuthHeaders(url, this.jwtHandler);
|
|
704
|
+
const response = await this.fetchFn(url, {
|
|
705
|
+
method: "PUT",
|
|
706
|
+
headers: {
|
|
707
|
+
...authHeaders,
|
|
708
|
+
"Content-Type": "application/octet-stream"
|
|
709
|
+
},
|
|
710
|
+
body: data,
|
|
711
|
+
duplex: "half"
|
|
712
|
+
});
|
|
713
|
+
if (!response.ok) throw new Error(`Attachment upload failed: ${response.status} ${response.statusText}`);
|
|
714
|
+
return await response.json();
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/switchboard/remote-attachment-upload-factory.ts
|
|
719
|
+
var RemoteAttachmentUploadFactory = class {
|
|
720
|
+
constructor(config) {
|
|
721
|
+
this.config = config;
|
|
722
|
+
}
|
|
723
|
+
createUpload(reservationId, options) {
|
|
724
|
+
return new RemoteAttachmentUpload(reservationId, options, this.config);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/switchboard/remote-attachment-store.ts
|
|
729
|
+
function isRecord(value) {
|
|
730
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
731
|
+
}
|
|
732
|
+
function isAttachmentMetadata(value) {
|
|
733
|
+
if (!isRecord(value)) return false;
|
|
734
|
+
if (typeof value.mimeType !== "string") return false;
|
|
735
|
+
if (typeof value.fileName !== "string") return false;
|
|
736
|
+
if (typeof value.sizeBytes !== "number" || !Number.isFinite(value.sizeBytes) || value.sizeBytes < 0) return false;
|
|
737
|
+
if (value.extension !== null && typeof value.extension !== "string") return false;
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
function contentTypeFallback(response) {
|
|
741
|
+
const contentLength = response.headers.get("Content-Length");
|
|
742
|
+
if (contentLength === null) throw new Error("Switchboard response missing both X-Attachment-Metadata and Content-Length headers");
|
|
743
|
+
const sizeBytes = Number(contentLength);
|
|
744
|
+
if (!Number.isInteger(sizeBytes) || sizeBytes < 0) throw new Error(`Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`);
|
|
745
|
+
const lastModified = response.headers.get("Last-Modified");
|
|
746
|
+
const dateHeader = response.headers.get("Date");
|
|
747
|
+
const createdAtUtc = lastModified ? new Date(lastModified).toISOString() : dateHeader ? new Date(dateHeader).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
748
|
+
return {
|
|
749
|
+
mimeType: response.headers.get("Content-Type") ?? "application/octet-stream",
|
|
750
|
+
fileName: "unknown",
|
|
751
|
+
sizeBytes,
|
|
752
|
+
extension: null,
|
|
753
|
+
createdAtUtc
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function parseMetadata(response) {
|
|
757
|
+
const metaHeader = response.headers.get("X-Attachment-Metadata");
|
|
758
|
+
if (metaHeader) try {
|
|
759
|
+
const parsed = JSON.parse(metaHeader);
|
|
760
|
+
if (isRecord(parsed) && parsed.extension === void 0) parsed.extension = null;
|
|
761
|
+
if (isAttachmentMetadata(parsed)) return parsed;
|
|
762
|
+
} catch {}
|
|
763
|
+
return contentTypeFallback(response);
|
|
764
|
+
}
|
|
765
|
+
var RemoteAttachmentStore = class {
|
|
766
|
+
remoteUrl;
|
|
767
|
+
jwtHandler;
|
|
768
|
+
fetchFn;
|
|
769
|
+
constructor(config) {
|
|
770
|
+
this.remoteUrl = config.remoteUrl;
|
|
771
|
+
this.jwtHandler = config.jwtHandler;
|
|
772
|
+
this.fetchFn = config.fetchFn ?? globalThis.fetch;
|
|
773
|
+
}
|
|
774
|
+
async stat(hash) {
|
|
775
|
+
const { header, body } = await this.fetchAttachment(hash);
|
|
776
|
+
await body.cancel();
|
|
777
|
+
return header;
|
|
778
|
+
}
|
|
779
|
+
async get(hash, signal) {
|
|
780
|
+
return this.fetchAttachment(hash, signal);
|
|
781
|
+
}
|
|
782
|
+
async fetchAttachment(hash, signal) {
|
|
783
|
+
const url = `${this.remoteUrl}/attachments/${hash}`;
|
|
784
|
+
const headers = await buildAuthHeaders(url, this.jwtHandler);
|
|
785
|
+
const response = await this.fetchFn(url, {
|
|
786
|
+
signal,
|
|
787
|
+
headers
|
|
788
|
+
});
|
|
789
|
+
if (response.status === 404) throw new AttachmentNotFound(hash);
|
|
790
|
+
if (!response.ok) throw new Error(`Attachment fetch failed: ${response.status} ${response.statusText}`);
|
|
791
|
+
if (!response.body) throw new Error("Response body is null");
|
|
792
|
+
const metadata = parseMetadata(response);
|
|
793
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
520
794
|
return {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
795
|
+
header: {
|
|
796
|
+
hash,
|
|
797
|
+
mimeType: metadata.mimeType,
|
|
798
|
+
fileName: metadata.fileName,
|
|
799
|
+
sizeBytes: metadata.sizeBytes,
|
|
800
|
+
extension: metadata.extension,
|
|
801
|
+
status: "available",
|
|
802
|
+
source: "sync",
|
|
803
|
+
createdAtUtc: now,
|
|
804
|
+
lastAccessedAtUtc: now
|
|
805
|
+
},
|
|
806
|
+
body: response.body
|
|
525
807
|
};
|
|
526
808
|
}
|
|
527
809
|
};
|
|
528
810
|
//#endregion
|
|
811
|
+
//#region src/switchboard/create-remote-attachment-service.ts
|
|
812
|
+
function createRemoteAttachmentService(config) {
|
|
813
|
+
const reservations = new RemoteReservationStore(config);
|
|
814
|
+
const uploadFactory = new RemoteAttachmentUploadFactory(config);
|
|
815
|
+
return new AttachmentService(new RemoteAttachmentStore(config), reservations, uploadFactory);
|
|
816
|
+
}
|
|
817
|
+
//#endregion
|
|
529
818
|
//#region src/null-attachment-transport.ts
|
|
530
819
|
/**
|
|
531
820
|
* No-op transport for deployments without remote sync.
|
|
@@ -547,6 +836,7 @@ var NullAttachmentTransport = class {
|
|
|
547
836
|
var AttachmentBuilder = class {
|
|
548
837
|
transport = new NullAttachmentTransport();
|
|
549
838
|
customUploadFactory;
|
|
839
|
+
maxUploadBytes;
|
|
550
840
|
constructor(db, storagePath) {
|
|
551
841
|
this.db = db;
|
|
552
842
|
this.storagePath = storagePath;
|
|
@@ -559,13 +849,17 @@ var AttachmentBuilder = class {
|
|
|
559
849
|
this.customUploadFactory = factory;
|
|
560
850
|
return this;
|
|
561
851
|
}
|
|
852
|
+
withMaxUploadBytes(maxBytes) {
|
|
853
|
+
this.maxUploadBytes = maxBytes;
|
|
854
|
+
return this;
|
|
855
|
+
}
|
|
562
856
|
async build() {
|
|
563
857
|
const result = await runAttachmentMigrations(this.db, ATTACHMENT_SCHEMA);
|
|
564
858
|
if (!result.success && result.error) throw result.error;
|
|
565
859
|
const scopedDb = this.db.withSchema(ATTACHMENT_SCHEMA);
|
|
566
860
|
const store = new KyselyAttachmentStore(scopedDb, this.transport, this.storagePath);
|
|
567
861
|
const reservations = new KyselyReservationStore(scopedDb);
|
|
568
|
-
const uploadFactory = this.customUploadFactory ?? new DirectAttachmentUploadFactory(scopedDb, this.storagePath, reservations);
|
|
862
|
+
const uploadFactory = this.customUploadFactory ?? new DirectAttachmentUploadFactory(scopedDb, this.storagePath, reservations, this.maxUploadBytes);
|
|
569
863
|
return {
|
|
570
864
|
service: new AttachmentService(store, reservations, uploadFactory),
|
|
571
865
|
store,
|
|
@@ -575,6 +869,6 @@ var AttachmentBuilder = class {
|
|
|
575
869
|
}
|
|
576
870
|
};
|
|
577
871
|
//#endregion
|
|
578
|
-
export { ATTACHMENT_SCHEMA, AttachmentBuilder, AttachmentNotFound, AttachmentService, DirectAttachmentUpload, DirectAttachmentUploadFactory, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, ReservationNotFound, SwitchboardAttachmentTransport, createRef, parseRef, runAttachmentMigrations };
|
|
872
|
+
export { ATTACHMENT_SCHEMA, AttachmentBuilder, AttachmentNotFound, AttachmentService, DirectAttachmentUpload, DirectAttachmentUploadFactory, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, ReservationNotFound, SwitchboardAttachmentTransport, createRef, createRemoteAttachmentService, parseRef, runAttachmentMigrations };
|
|
579
873
|
|
|
580
874
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["rowToHeader","up","down","migration001","migration002"],"sources":["../src/errors.ts","../src/ref.ts","../src/attachment-service.ts","../src/storage/fs/attachment-fs.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/001_create_attachment_table.ts","../src/storage/migrations/002_create_reservation_table.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/switchboard/switchboard-attachment-transport.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"sourcesContent":["/**\n * Thrown when an attachment ref or hash is not known to the store.\n */\nexport class AttachmentNotFound extends Error {\n constructor(identifier: string) {\n super(`Attachment not found: ${identifier}`);\n this.name = \"AttachmentNotFound\";\n }\n}\n\n/**\n * Thrown when a reservation ID is not found in the reservation store.\n */\nexport class ReservationNotFound extends Error {\n constructor(reservationId: string) {\n super(`Reservation not found: ${reservationId}`);\n this.name = \"ReservationNotFound\";\n }\n}\n\n/**\n * Thrown when an attachment ref string does not match the expected format.\n */\nexport class InvalidAttachmentRef extends Error {\n constructor(ref: string) {\n super(`Invalid attachment ref: ${ref}`);\n this.name = \"InvalidAttachmentRef\";\n }\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport { InvalidAttachmentRef } from \"./errors.js\";\n\nconst REF_PATTERN = /^attachment:\\/\\/v(\\d+):(.+)$/;\nconst DEFAULT_VERSION = 1;\n\nexport type ParsedRef = {\n version: number;\n hash: AttachmentHash;\n};\n\nexport function parseRef(ref: AttachmentRef): ParsedRef {\n const match = REF_PATTERN.exec(ref);\n if (!match) {\n throw new InvalidAttachmentRef(ref);\n }\n return {\n version: Number(match[1]),\n hash: match[2],\n };\n}\n\nexport function createRef(\n hash: AttachmentHash,\n version: number = DEFAULT_VERSION,\n): AttachmentRef {\n return `attachment://v${version}:${hash}`;\n}\n","import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentService,\n IAttachmentStore,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentResponse,\n ReserveAttachmentOptions,\n} from \"./types.js\";\nimport { parseRef } from \"./ref.js\";\n\nexport class AttachmentService implements IAttachmentService {\n constructor(\n private readonly store: IAttachmentStore,\n private readonly reservations: IReservationStore,\n private readonly uploadFactory: IAttachmentUploadFactory,\n ) {}\n\n async reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload> {\n const reservation = await this.reservations.create(options);\n return this.uploadFactory.createUpload(reservation.reservationId, options);\n }\n\n async stat(ref: AttachmentRef): Promise<AttachmentHeader> {\n const { hash } = parseRef(ref);\n return this.store.stat(hash);\n }\n\n async get(\n ref: AttachmentRef,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const { hash } = parseRef(ref);\n return this.store.get(hash, signal);\n }\n}\n","import { mkdir, rm, access } from \"node:fs/promises\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { Readable } from \"node:stream\";\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 * 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 writer = createWriteStream(path);\n const reader = data.getReader();\n let bytesWritten = 0;\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesWritten += value.byteLength;\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve) => writer.once(\"drain\", resolve));\n }\n }\n } finally {\n reader.releaseLock();\n await new Promise<void>((resolve, reject) => {\n writer.end(() => resolve());\n writer.on(\"error\", reject);\n });\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","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 } 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 };\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 throw new AttachmentNotFound(hash);\n }\n\n return rowToHeader(row);\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 throw new AttachmentNotFound(hash);\n }\n\n if (row.status === \"evicted\") {\n const remote = await this.transport.fetch(hash, signal);\n if (!remote) {\n throw new AttachmentNotFound(hash);\n }\n await this.put(hash, remote.metadata, remote.body);\n return this.get(hash, signal);\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 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: now,\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: active reader tracking\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\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 };\n}\n\nexport class KyselyReservationStore implements IReservationStore {\n constructor(private readonly db: Kysely<AttachmentDatabase>) {}\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const reservationId = randomUUID();\n const now = new Date().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 })\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 .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 .deleteFrom(\"attachment_reservation\")\n .where(\"reservation_id\", \"=\", reservationId)\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\")\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 { 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\";\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};\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 { createHash } from \"node:crypto\";\nimport { join } from \"node:path\";\nimport type { Kysely } from \"kysely\";\nimport type { IAttachmentUpload, IReservationStore } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentUploadResult,\n ReserveAttachmentOptions,\n} from \"../types.js\";\nimport type {\n AttachmentDatabase,\n AttachmentRow,\n} from \"../storage/kysely/types.js\";\nimport { createRef } from \"../ref.js\";\nimport {\n storageRelativePath,\n writeAttachmentBytes,\n streamFromBuffer,\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 };\n}\n\nasync function collectAndHash(\n data: ReadableStream<Uint8Array>,\n): Promise<{ bytes: Uint8Array; hash: string }> {\n const hasher = createHash(\"sha256\");\n const chunks: Uint8Array[] = [];\n const reader = data.getReader();\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n hasher.update(value);\n chunks.push(value);\n }\n\n const totalLength = chunks.reduce((acc, c) => acc + c.byteLength, 0);\n const bytes = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return { bytes, hash: hasher.digest(\"hex\") };\n}\n\nexport class DirectAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n\n constructor(\n reservationId: string,\n private readonly options: ReserveAttachmentOptions,\n private readonly db: Kysely<AttachmentDatabase>,\n private readonly basePath: string,\n private readonly reservations: IReservationStore,\n ) {\n this.reservationId = reservationId;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n const { bytes, hash } = await collectAndHash(data);\n\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 const relPath = storageRelativePath(hash);\n const fullPath = join(this.basePath, relPath);\n await writeAttachmentBytes(fullPath, streamFromBuffer(bytes));\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.options.mimeType,\n file_name: this.options.fileName,\n size_bytes: bytes.byteLength,\n extension: this.options.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\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 { ReserveAttachmentOptions } 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 ) {}\n\n createUpload(\n reservationId: string,\n options: ReserveAttachmentOptions,\n ): IAttachmentUpload {\n return new DirectAttachmentUpload(\n reservationId,\n options,\n this.db,\n this.basePath,\n this.reservations,\n );\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IAttachmentTransport } from \"../interfaces.js\";\nimport type { AttachmentMetadata, TransportResponse } from \"../types.js\";\n\nexport type SwitchboardTransportConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nexport class SwitchboardAttachmentTransport implements IAttachmentTransport {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardTransportConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async fetch(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<TransportResponse | null> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await this.buildHeaders(url);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 404) {\n return null;\n }\n\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const metadata = this.parseMetadataHeaders(response);\n const body = response.body;\n if (!body) {\n throw new Error(\"Response body is null\");\n }\n\n return { hash, metadata, body };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async announce(_hash: AttachmentHash): Promise<void> {\n // No-op for switchboard -- data is already on the server after upload.\n }\n\n async push(\n hash: AttachmentHash,\n remote: string,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const url = `${remote}/attachments/${hash}`;\n const headers = await this.buildHeaders(url);\n\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n body: data,\n headers,\n // @ts-expect-error Node fetch requires duplex for streaming request bodies\n duplex: \"half\",\n });\n\n if (!response.ok) {\n throw new Error(\n `Attachment push failed: ${response.status} ${response.statusText}`,\n );\n }\n }\n\n private async buildHeaders(url: string): Promise<Record<string, string>> {\n const headers: Record<string, string> = {};\n if (this.jwtHandler) {\n const token = await this.jwtHandler(url);\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n }\n return headers;\n }\n\n private parseMetadataHeaders(response: Response): AttachmentMetadata {\n const metaHeader = response.headers.get(\"X-Attachment-Metadata\");\n if (metaHeader) {\n return JSON.parse(metaHeader) as AttachmentMetadata;\n }\n return {\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes: Number(response.headers.get(\"Content-Length\") ?? 0),\n extension: null,\n };\n }\n}\n","import type { IAttachmentTransport } from \"./interfaces.js\";\nimport type { TransportResponse } from \"./types.js\";\n\n/**\n * No-op transport for deployments without remote sync.\n * fetch() always returns null, announce() and push() are no-ops.\n */\nexport class NullAttachmentTransport implements IAttachmentTransport {\n fetch(): Promise<TransportResponse | null> {\n return Promise.resolve(null);\n }\n\n announce(): Promise<void> {\n return Promise.resolve();\n }\n\n push(): Promise<void> {\n return Promise.resolve();\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\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\";\n\nexport type AttachmentBuildResult = {\n service: AttachmentService;\n store: KyselyAttachmentStore;\n reservations: KyselyReservationStore;\n uploadFactory: IAttachmentUploadFactory;\n};\n\nexport class AttachmentBuilder {\n private transport: IAttachmentTransport = new NullAttachmentTransport();\n private customUploadFactory?: IAttachmentUploadFactory;\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 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 new DirectAttachmentUploadFactory(\n scopedDb,\n this.storagePath,\n reservations,\n );\n\n const service = new AttachmentService(store, reservations, uploadFactory);\n\n return { service, store, reservations, uploadFactory };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["rowToHeader","up","down","up","down","migration001","migration002","migration003","isRecord","isAttachmentMetadata","contentTypeFallback"],"sources":["../src/errors.ts","../src/ref.ts","../src/attachment-service.ts","../src/storage/fs/attachment-fs.ts","../src/storage/kysely/attachment-store.ts","../src/storage/kysely/reservation-store.ts","../src/storage/migrations/001_create_attachment_table.ts","../src/storage/migrations/002_create_reservation_table.ts","../src/storage/migrations/003_add_reservation_expires_at.ts","../src/storage/migrations/migrator.ts","../src/direct/direct-attachment-upload.ts","../src/direct/direct-attachment-upload-factory.ts","../src/switchboard/build-auth-headers.ts","../src/switchboard/switchboard-attachment-transport.ts","../src/switchboard/remote-reservation-store.ts","../src/switchboard/remote-attachment-upload.ts","../src/switchboard/remote-attachment-upload-factory.ts","../src/switchboard/remote-attachment-store.ts","../src/switchboard/create-remote-attachment-service.ts","../src/null-attachment-transport.ts","../src/attachment-builder.ts"],"sourcesContent":["/**\n * Thrown when an attachment ref or hash is not known to the store.\n */\nexport class AttachmentNotFound extends Error {\n constructor(identifier: string) {\n super(`Attachment not found: ${identifier}`);\n this.name = \"AttachmentNotFound\";\n }\n}\n\n/**\n * Thrown when a reservation ID is not found in the reservation store.\n */\nexport class ReservationNotFound extends Error {\n constructor(reservationId: string) {\n super(`Reservation not found: ${reservationId}`);\n this.name = \"ReservationNotFound\";\n }\n}\n\n/**\n * Thrown when an attachment ref string does not match the expected format.\n */\nexport class InvalidAttachmentRef extends Error {\n constructor(ref: string) {\n super(`Invalid attachment ref: ${ref}`);\n this.name = \"InvalidAttachmentRef\";\n }\n}\n\n/**\n * Thrown when an upload exceeds the configured maximum byte cap.\n * Route handlers should map this to HTTP 413 Payload Too Large.\n */\nexport class UploadTooLarge extends Error {\n readonly maxBytes: number;\n constructor(maxBytes: number) {\n super(`Upload exceeds maximum size of ${maxBytes} bytes`);\n this.name = \"UploadTooLarge\";\n this.maxBytes = maxBytes;\n }\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport { InvalidAttachmentRef } from \"./errors.js\";\n\nconst REF_PATTERN = /^attachment:\\/\\/v(\\d+):(.+)$/;\nconst DEFAULT_VERSION = 1;\n\nexport type ParsedRef = {\n version: number;\n hash: AttachmentHash;\n};\n\nexport function parseRef(ref: AttachmentRef): ParsedRef {\n const match = REF_PATTERN.exec(ref);\n if (!match) {\n throw new InvalidAttachmentRef(ref);\n }\n return {\n version: Number(match[1]),\n hash: match[2],\n };\n}\n\nexport function createRef(\n hash: AttachmentHash,\n version: number = DEFAULT_VERSION,\n): AttachmentRef {\n return `attachment://v${version}:${hash}`;\n}\n","import type { AttachmentRef } from \"@powerhousedao/reactor\";\nimport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentResponse,\n ReserveAttachmentOptions,\n} from \"./types.js\";\nimport { parseRef } from \"./ref.js\";\n\nexport class AttachmentService implements IAttachmentService {\n constructor(\n private readonly store: IAttachmentReader,\n private readonly reservations: IReservationStore,\n private readonly uploadFactory: IAttachmentUploadFactory,\n ) {}\n\n async reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload> {\n const reservation = await this.reservations.create(options);\n return this.uploadFactory.createUpload(reservation.reservationId, options);\n }\n\n async stat(ref: AttachmentRef): Promise<AttachmentHeader> {\n const { hash } = parseRef(ref);\n return this.store.stat(hash);\n }\n\n async get(\n ref: AttachmentRef,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const { hash } = parseRef(ref);\n return this.store.get(hash, signal);\n }\n}\n","import { mkdir, rm, 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 { 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 * 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 writer = createWriteStream(path);\n const reader = data.getReader();\n let bytesWritten = 0;\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesWritten += value.byteLength;\n const canContinue = writer.write(value);\n if (!canContinue) {\n await new Promise<void>((resolve) => writer.once(\"drain\", resolve));\n }\n }\n } finally {\n reader.releaseLock();\n await new Promise<void>((resolve, reject) => {\n writer.end(() => resolve());\n writer.on(\"error\", reject);\n });\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 */\nexport async function streamHashAndWrite(\n basePath: string,\n data: ReadableStream<Uint8Array>,\n options: { maxBytes?: number } = {},\n): Promise<{ tempPath: string; hash: string; sizeBytes: number }> {\n const { maxBytes } = options;\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 let sizeBytes = 0;\n let caughtError: Error | undefined;\n\n try {\n for (;;) {\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 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 } catch (err) {\n caughtError = err instanceof Error ? err : new Error(String(err));\n } finally {\n reader.releaseLock();\n }\n\n await new Promise<void>((resolve, reject) => {\n writer.end((err?: Error | null) => {\n if (err) reject(err);\n else resolve();\n });\n writer.on(\"error\", reject);\n });\n\n if (caughtError) {\n await rm(tempPath, { force: true });\n throw caughtError;\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 } 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 };\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 throw new AttachmentNotFound(hash);\n }\n\n return rowToHeader(row);\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 throw new AttachmentNotFound(hash);\n }\n\n if (row.status === \"evicted\") {\n const remote = await this.transport.fetch(hash, signal);\n if (!remote) {\n throw new AttachmentNotFound(hash);\n }\n await this.put(hash, remote.metadata, remote.body);\n return this.get(hash, signal);\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 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: active reader tracking\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 };\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 })\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 .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 .deleteFrom(\"attachment_reservation\")\n .where(\"reservation_id\", \"=\", reservationId)\n .execute();\n }\n\n async deleteExpired(now: Date = new Date()): Promise<number> {\n const result = await this.db\n .deleteFrom(\"attachment_reservation\")\n .where(\"expires_at_utc\", \"<=\", now.toISOString())\n .executeTakeFirst();\n\n return Number(result.numDeletedRows ?? 0);\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 { 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\";\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};\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 { IAttachmentUpload, IReservationStore } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentUploadResult,\n ReserveAttachmentOptions,\n} from \"../types.js\";\nimport type {\n AttachmentDatabase,\n AttachmentRow,\n} from \"../storage/kysely/types.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 };\n}\n\nexport class DirectAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n\n constructor(\n reservationId: string,\n private readonly options: ReserveAttachmentOptions,\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 = reservationId;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\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 const { tempPath, hash, sizeBytes } = await streamHashAndWrite(\n this.basePath,\n data,\n { maxBytes: this.maxBytes },\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.options.mimeType,\n file_name: this.options.fileName,\n size_bytes: sizeBytes,\n extension: this.options.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 { ReserveAttachmentOptions } 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(\n reservationId: string,\n options: ReserveAttachmentOptions,\n ): IAttachmentUpload {\n return new DirectAttachmentUpload(\n reservationId,\n options,\n this.db,\n this.basePath,\n this.reservations,\n this.maxBytes,\n );\n }\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\n\nexport async function buildAuthHeaders(\n url: string,\n jwtHandler: JwtHandler | undefined,\n): Promise<Record<string, string>> {\n const headers: Record<string, string> = {};\n if (jwtHandler) {\n const token = await jwtHandler(url);\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IAttachmentTransport } from \"../interfaces.js\";\nimport type { AttachmentMetadata, TransportResponse } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\n\nexport type SwitchboardTransportConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nexport class SwitchboardAttachmentTransport implements IAttachmentTransport {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardTransportConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async fetch(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<TransportResponse | null> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 404) {\n return null;\n }\n\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const metadata = this.parseMetadataHeaders(response);\n const body = response.body;\n if (!body) {\n throw new Error(\"Response body is null\");\n }\n\n return { hash, metadata, body };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async announce(_hash: AttachmentHash): Promise<void> {\n // No-op for switchboard -- data is already on the server after upload.\n }\n\n async push(\n hash: AttachmentHash,\n remote: string,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const url = `${remote}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n body: data,\n headers,\n // @ts-expect-error Node fetch requires duplex for streaming request bodies\n duplex: \"half\",\n });\n\n if (!response.ok) {\n throw new Error(\n `Attachment push failed: ${response.status} ${response.statusText}`,\n );\n }\n }\n\n private parseMetadataHeaders(response: Response): AttachmentMetadata {\n const metaHeader = response.headers.get(\"X-Attachment-Metadata\");\n if (metaHeader) {\n try {\n const parsed: unknown = JSON.parse(metaHeader);\n if (isRecord(parsed) && parsed.extension === undefined) {\n parsed.extension = null;\n }\n if (isAttachmentMetadata(parsed)) {\n return parsed;\n }\n } catch {\n // fall through to Content-Type fallback\n }\n }\n return contentTypeFallback(response);\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isAttachmentMetadata(value: unknown): value is AttachmentMetadata {\n if (!isRecord(value)) return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (\n typeof value.sizeBytes !== \"number\" ||\n !Number.isFinite(value.sizeBytes) ||\n value.sizeBytes < 0\n ) {\n return false;\n }\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n return true;\n}\n\nfunction contentTypeFallback(response: Response): AttachmentMetadata {\n const contentLength = response.headers.get(\"Content-Length\");\n if (contentLength === null) {\n throw new Error(\n \"Switchboard response missing both X-Attachment-Metadata and Content-Length headers\",\n );\n }\n const sizeBytes = Number(contentLength);\n if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {\n throw new Error(\n `Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`,\n );\n }\n // Last-Modified is the closest legitimate signal we have for an original\n // creation time when X-Attachment-Metadata is absent. If that's missing too,\n // fall back to the response Date header (still server-attributed). This is\n // imperfect — Last-Modified reflects the most recent change, not the\n // original upload — but unlike sizeBytes there is no zero-equivalent\n // sentinel for a date, and downstream consumers expect a value.\n const lastModified = response.headers.get(\"Last-Modified\");\n const dateHeader = response.headers.get(\"Date\");\n const createdAtUtc = lastModified\n ? new Date(lastModified).toISOString()\n : dateHeader\n ? new Date(dateHeader).toISOString()\n : new Date().toISOString();\n\n return {\n // application/octet-stream is the RFC 2046 sentinel for \"unknown binary\",\n // and \"unknown\" is a non-real filename sentinel; neither is a fabricated\n // semantic value the way Content-Length=0 would be.\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes,\n extension: null,\n createdAtUtc,\n };\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IReservationStore } from \"../interfaces.js\";\nimport type { Reservation, ReserveAttachmentOptions } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\n\nexport type SwitchboardClientConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nexport class RemoteReservationStore implements IReservationStore {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardClientConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const url = `${this.remoteUrl}/attachments/reservations`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"POST\",\n headers: { ...authHeaders, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n mimeType: options.mimeType,\n fileName: options.fileName,\n extension: options.extension ?? null,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\n `Reservation create failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const json = (await response.json()) as {\n reservationId: string;\n createdAtUtc?: string;\n expiresAtUtc?: string;\n };\n // The server is the source of truth for both timestamps. We synthesize\n // only as a last-resort fallback for older switchboards that don't\n // include them in the response; in that case the client cannot know the\n // server's TTL, so expiresAtUtc is a best-effort placeholder.\n const now = new Date();\n return {\n reservationId: json.reservationId,\n mimeType: options.mimeType,\n fileName: options.fileName,\n extension: options.extension ?? null,\n createdAtUtc: json.createdAtUtc ?? now.toISOString(),\n expiresAtUtc:\n json.expiresAtUtc ??\n new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),\n };\n }\n\n get(_reservationId: string): Promise<Reservation> {\n return Promise.reject(\n new Error(\"RemoteReservationStore.get is not supported\"),\n );\n }\n\n delete(_reservationId: string): Promise<void> {\n return Promise.reject(\n new Error(\"RemoteReservationStore.delete is not supported\"),\n );\n }\n\n // Sweeping is the server's responsibility; clients have no authority to\n // delete reservations on a remote switchboard.\n deleteExpired(_now?: Date): Promise<number> {\n return Promise.reject(\n new Error(\"RemoteReservationStore.deleteExpired is not supported\"),\n );\n }\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IAttachmentUpload } from \"../interfaces.js\";\nimport type {\n AttachmentUploadResult,\n ReserveAttachmentOptions,\n} from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nexport class RemoteAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n // The reserve options are kept for symmetry with DirectAttachmentUpload,\n // but the server already has them tied to the reservation row.\n private readonly options: ReserveAttachmentOptions;\n\n constructor(\n reservationId: string,\n options: ReserveAttachmentOptions,\n config: SwitchboardClientConfig,\n ) {\n this.reservationId = reservationId;\n this.options = options;\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n const url = `${this.remoteUrl}/attachments/reservations/${this.reservationId}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n // Always upload as octet-stream. The server reads the real mime type from\n // the reservation row; sending the user's mime type here (e.g. application/json)\n // would let Express body-parser drain the request body before our handler runs,\n // silently writing zero bytes.\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n headers: { ...authHeaders, \"Content-Type\": \"application/octet-stream\" },\n body: data,\n // @ts-expect-error Node fetch requires duplex for streaming request bodies\n duplex: \"half\",\n });\n\n if (!response.ok) {\n throw new Error(\n `Attachment upload failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return (await response.json()) as AttachmentUploadResult;\n }\n}\n","import type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n} from \"../interfaces.js\";\nimport type { ReserveAttachmentOptions } from \"../types.js\";\nimport { RemoteAttachmentUpload } from \"./remote-attachment-upload.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nexport class RemoteAttachmentUploadFactory implements IAttachmentUploadFactory {\n constructor(private readonly config: SwitchboardClientConfig) {}\n\n createUpload(\n reservationId: string,\n options: ReserveAttachmentOptions,\n ): IAttachmentUpload {\n return new RemoteAttachmentUpload(reservationId, options, this.config);\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport { AttachmentNotFound } from \"../errors.js\";\nimport type { IAttachmentReader } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n} from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isAttachmentMetadata(value: unknown): value is AttachmentMetadata {\n if (!isRecord(value)) return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (\n typeof value.sizeBytes !== \"number\" ||\n !Number.isFinite(value.sizeBytes) ||\n value.sizeBytes < 0\n ) {\n return false;\n }\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n return true;\n}\n\nfunction contentTypeFallback(response: Response): AttachmentMetadata {\n const contentLength = response.headers.get(\"Content-Length\");\n if (contentLength === null) {\n throw new Error(\n \"Switchboard response missing both X-Attachment-Metadata and Content-Length headers\",\n );\n }\n const sizeBytes = Number(contentLength);\n if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {\n throw new Error(\n `Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`,\n );\n }\n // Last-Modified is the closest legitimate signal we have for an original\n // creation time when X-Attachment-Metadata is absent. If that's missing too,\n // fall back to the response date (still server-attributed). This is\n // imperfect — Last-Modified reflects the most recent change, not the\n // original upload — but unlike sizeBytes there is no zero-equivalent\n // sentinel for a date, and downstream consumers expect a value.\n const lastModified = response.headers.get(\"Last-Modified\");\n const dateHeader = response.headers.get(\"Date\");\n const createdAtUtc = lastModified\n ? new Date(lastModified).toISOString()\n : dateHeader\n ? new Date(dateHeader).toISOString()\n : new Date().toISOString();\n\n return {\n // application/octet-stream is the RFC 2046 sentinel for \"unknown binary\",\n // and \"unknown\" is a non-real filename sentinel; neither is a fabricated\n // semantic value the way Content-Length=0 would be.\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes,\n extension: null,\n createdAtUtc,\n };\n}\n\nfunction parseMetadata(response: Response): AttachmentMetadata {\n const metaHeader = response.headers.get(\"X-Attachment-Metadata\");\n if (metaHeader) {\n try {\n const parsed: unknown = JSON.parse(metaHeader);\n if (isRecord(parsed) && parsed.extension === undefined) {\n parsed.extension = null;\n }\n if (isAttachmentMetadata(parsed)) {\n return parsed;\n }\n } catch {\n // fall through to Content-Type fallback\n }\n }\n return contentTypeFallback(response);\n}\n\nexport class RemoteAttachmentStore implements IAttachmentReader {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardClientConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n }\n\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const { header, body } = await this.fetchAttachment(hash);\n await body.cancel();\n return header;\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n return this.fetchAttachment(hash, signal);\n }\n\n private async fetchAttachment(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 404) {\n throw new AttachmentNotFound(hash);\n }\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n const metadata = parseMetadata(response);\n const now = new Date().toISOString();\n const header: AttachmentHeader = {\n hash,\n mimeType: metadata.mimeType,\n fileName: metadata.fileName,\n sizeBytes: metadata.sizeBytes,\n extension: metadata.extension,\n status: \"available\",\n source: \"sync\",\n createdAtUtc: now,\n lastAccessedAtUtc: now,\n };\n\n return { header, body: response.body };\n }\n}\n","import { AttachmentService } from \"../attachment-service.js\";\nimport type { IAttachmentService } from \"../interfaces.js\";\nimport { RemoteAttachmentStore } from \"./remote-attachment-store.js\";\nimport { RemoteAttachmentUploadFactory } from \"./remote-attachment-upload-factory.js\";\nimport {\n RemoteReservationStore,\n type SwitchboardClientConfig,\n} from \"./remote-reservation-store.js\";\n\nexport function createRemoteAttachmentService(\n config: SwitchboardClientConfig,\n): IAttachmentService {\n const reservations = new RemoteReservationStore(config);\n const uploadFactory = new RemoteAttachmentUploadFactory(config);\n const store = new RemoteAttachmentStore(config);\n return new AttachmentService(store, reservations, uploadFactory);\n}\n","import type { IAttachmentTransport } from \"./interfaces.js\";\nimport type { TransportResponse } from \"./types.js\";\n\n/**\n * No-op transport for deployments without remote sync.\n * fetch() always returns null, announce() and push() are no-ops.\n */\nexport class NullAttachmentTransport implements IAttachmentTransport {\n fetch(): Promise<TransportResponse | null> {\n return Promise.resolve(null);\n }\n\n announce(): Promise<void> {\n return Promise.resolve();\n }\n\n push(): Promise<void> {\n return Promise.resolve();\n }\n}\n","import type { Kysely } from \"kysely\";\nimport type {\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\";\n\nexport type AttachmentBuildResult = {\n service: AttachmentService;\n store: KyselyAttachmentStore;\n reservations: KyselyReservationStore;\n uploadFactory: IAttachmentUploadFactory;\n};\n\nexport class AttachmentBuilder {\n private transport: IAttachmentTransport = new NullAttachmentTransport();\n private customUploadFactory?: IAttachmentUploadFactory;\n private maxUploadBytes?: number;\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 withMaxUploadBytes(maxBytes: number): this {\n this.maxUploadBytes = maxBytes;\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 new DirectAttachmentUploadFactory(\n scopedDb,\n this.storagePath,\n reservations,\n this.maxUploadBytes,\n );\n\n const service = new AttachmentService(store, reservations, uploadFactory);\n\n return { service, store, reservations, uploadFactory };\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/reactor-attachments",
|
|
3
|
-
"version": "6.0.0-dev.
|
|
3
|
+
"version": "6.0.0-dev.210",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"sideEffects": false,
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"kysely": "0.28.16",
|
|
25
|
-
"@powerhousedao/reactor": "6.0.0-dev.
|
|
25
|
+
"@powerhousedao/reactor": "6.0.0-dev.210"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@electric-sql/pglite": "0.3.15",
|