@beignet/provider-storage-local 0.0.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/provider-storage-local
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @beignet/provider-storage-local
2
+
3
+ Local filesystem storage provider for Beignet.
4
+
5
+ The provider installs the app-facing `ctx.ports.storage` port. Use it for
6
+ local development, tests that need durable files, and small deployments where a
7
+ filesystem-backed object store is enough.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun add @beignet/provider-storage-local
13
+ ```
14
+
15
+ ## Provider setup
16
+
17
+ ```typescript
18
+ import { localStorageProvider } from "@beignet/provider-storage-local";
19
+ import { createServer } from "@beignet/core/server";
20
+
21
+ const server = await createServer({
22
+ ports: basePorts,
23
+ providers: [localStorageProvider],
24
+ createContext: ({ ports }) => ({ ports }),
25
+ routes,
26
+ });
27
+ ```
28
+
29
+ Environment variables:
30
+
31
+ | Variable | Description |
32
+ | --- | --- |
33
+ | `STORAGE_ROOT` | Directory where objects are written. Defaults to `storage/app`. |
34
+ | `STORAGE_PUBLIC_BASE_URL` | Optional base URL returned by `publicUrl(...)` for public objects. |
35
+
36
+ `STORAGE_PUBLIC_BASE_URL` may be an absolute URL such as
37
+ `https://assets.example.com` or an app-relative path such as `/storage`.
38
+ The provider only returns URLs. If you use an app-relative path in a Next.js
39
+ app, serve public objects with `createStorageRoute`:
40
+
41
+ ```typescript
42
+ // app/storage/[...key]/route.ts
43
+ import { createStorageRoute } from "@beignet/next";
44
+ import { server } from "@/server";
45
+
46
+ export const { GET, HEAD } = createStorageRoute(server.ports.storage, {
47
+ basePath: "/storage",
48
+ });
49
+ ```
50
+
51
+ ## Direct port factory
52
+
53
+ ```typescript
54
+ import { createLocalStorage } from "@beignet/provider-storage-local";
55
+
56
+ const storage = createLocalStorage({
57
+ root: "storage/app",
58
+ publicBaseUrl: "/storage",
59
+ });
60
+ ```
61
+
62
+ The same `StoragePort` works with local files, memory tests, and cloud object
63
+ stores:
64
+
65
+ ```typescript
66
+ await ctx.ports.storage.put("avatars/user_123.png", avatarBytes, {
67
+ contentType: "image/png",
68
+ visibility: "public",
69
+ });
70
+
71
+ const object = await ctx.ports.storage.get("avatars/user_123.png");
72
+ const url = await ctx.ports.storage.publicUrl("avatars/user_123.png");
73
+ ```
74
+
75
+ ## Files on disk
76
+
77
+ Objects are written below `root` using their storage key. Object metadata is
78
+ stored in a `.beignet-storage-meta` sidecar directory below the same root.
79
+
80
+ Storage keys must be relative object keys: no empty strings, empty path
81
+ segments, leading or trailing `/`, backslashes, or `.` / `..` path segments.
82
+ The `.beignet-storage-meta` path segment is reserved for the provider's sidecar
83
+ metadata.
84
+
85
+ ## Devtools
86
+
87
+ When `ctx.ports.devtools` is installed, the provider records storage
88
+ operations under the `storage` watcher. Events include operation name, key,
89
+ duration, object size, visibility, and whether a lookup hit. Object bodies are
90
+ never recorded.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,55 @@
1
+ import type { StoragePort } from "@beignet/core/ports";
2
+ import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
3
+ import { z } from "zod";
4
+ export type { StorageBody, StorageMetadata, StorageObject, StorageObjectBody, StoragePort, StorageVisibility, } from "@beignet/core/ports";
5
+ declare const LocalStorageConfigSchema: z.ZodObject<{
6
+ ROOT: z.ZodDefault<z.ZodString>;
7
+ PUBLIC_BASE_URL: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strip>;
9
+ export type LocalStorageConfig = z.infer<typeof LocalStorageConfigSchema>;
10
+ export interface LocalStorageOptions {
11
+ /**
12
+ * Directory where storage objects are written.
13
+ */
14
+ root: string;
15
+ /**
16
+ * Base URL used by `publicUrl(...)` for public objects.
17
+ */
18
+ publicBaseUrl?: string;
19
+ /**
20
+ * Optional instrumentation target. The provider passes existing app ports
21
+ * automatically; direct factory users can pass an instrumentation port.
22
+ */
23
+ instrumentation?: ProviderInstrumentationTarget;
24
+ }
25
+ export interface LocalStorageProviderOptions {
26
+ /**
27
+ * Provider name. Defaults to "storage-local".
28
+ */
29
+ name?: string;
30
+ /**
31
+ * Default root used when STORAGE_ROOT is not set.
32
+ */
33
+ root?: string;
34
+ /**
35
+ * Default public base URL used when STORAGE_PUBLIC_BASE_URL is not set.
36
+ */
37
+ publicBaseUrl?: string;
38
+ }
39
+ export declare function createLocalStorage(options: LocalStorageOptions): StoragePort;
40
+ export interface LocalStorageProviderPorts {
41
+ storage: StoragePort;
42
+ }
43
+ export declare function createLocalStorageProvider(options?: LocalStorageProviderOptions): import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
44
+ PUBLIC_BASE_URL: z.ZodOptional<z.ZodString>;
45
+ ROOT: z.ZodDefault<z.ZodString>;
46
+ }, z.core.$strip>, {
47
+ storage: StoragePort;
48
+ }>;
49
+ export declare const localStorageProvider: import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
50
+ PUBLIC_BASE_URL: z.ZodOptional<z.ZodString>;
51
+ ROOT: z.ZodDefault<z.ZodString>;
52
+ }, z.core.$strip>, {
53
+ storage: StoragePort;
54
+ }>;
55
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAKV,WAAW,EAEZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,YAAY,EACV,WAAW,EACX,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAI7B,QAAA,MAAM,wBAAwB;;;iBAG5B,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,eAAe,CAAC,EAAE,6BAA6B,CAAC;CACjD;AAED,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA8UD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,WAAW,CA4Q5E;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,2BAAgC;;;;;GAoC1C;AAED,eAAO,MAAM,oBAAoB;;;;;EAA+B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,527 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createReadStream, createWriteStream } from "node:fs";
3
+ import { mkdir, readFile, rename, rm, stat as statFile, writeFile, } from "node:fs/promises";
4
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
8
+ import { z } from "zod";
9
+ const metadataRootName = ".beignet-storage-meta";
10
+ const LocalStorageConfigSchema = z.object({
11
+ ROOT: z.string().min(1).default("storage/app"),
12
+ PUBLIC_BASE_URL: z.string().min(1).optional(),
13
+ });
14
+ function copyBytes(bytes) {
15
+ return new Uint8Array(bytes);
16
+ }
17
+ function bytesToArrayBuffer(bytes) {
18
+ const buffer = new ArrayBuffer(bytes.byteLength);
19
+ new Uint8Array(buffer).set(bytes);
20
+ return buffer;
21
+ }
22
+ function hasControlCharacter(value) {
23
+ for (const char of value) {
24
+ const code = char.charCodeAt(0);
25
+ if (code <= 31 || code === 127)
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ async function writeStorageBody(filePath, body) {
31
+ if (typeof body === "string") {
32
+ await writeFile(filePath, body);
33
+ return;
34
+ }
35
+ if (body instanceof Uint8Array) {
36
+ await writeFile(filePath, copyBytes(body));
37
+ return;
38
+ }
39
+ if (body instanceof ArrayBuffer) {
40
+ await writeFile(filePath, new Uint8Array(body.slice(0)));
41
+ return;
42
+ }
43
+ if (body instanceof Blob) {
44
+ await pipeline(Readable.fromWeb(body.stream()), createWriteStream(filePath));
45
+ return;
46
+ }
47
+ await pipeline(Readable.fromWeb(body), createWriteStream(filePath));
48
+ }
49
+ function validateStorageKey(key) {
50
+ if (key.length === 0) {
51
+ throw new Error("Storage key must not be empty.");
52
+ }
53
+ if (hasControlCharacter(key)) {
54
+ throw new Error("Storage key must not include control characters.");
55
+ }
56
+ if (key.startsWith("/")) {
57
+ throw new Error("Storage key must not start with '/'.");
58
+ }
59
+ if (key.endsWith("/")) {
60
+ throw new Error("Storage key must not end with '/'.");
61
+ }
62
+ if (key.includes("\\")) {
63
+ throw new Error("Storage key must use '/' separators, not '\\'.");
64
+ }
65
+ const segments = key.split("/");
66
+ if (segments.some((segment) => segment === "")) {
67
+ throw new Error("Storage key must not include empty path segments.");
68
+ }
69
+ if (segments.some((segment) => segment === "." || segment === "..")) {
70
+ throw new Error("Storage key must not include '.' or '..' segments.");
71
+ }
72
+ if (segments.some((segment) => segment === metadataRootName)) {
73
+ throw new Error(`Local storage key must not use reserved segment '${metadataRootName}'.`);
74
+ }
75
+ }
76
+ function joinPublicUrl(baseUrl, key) {
77
+ const base = baseUrl.replace(/\/+$/, "");
78
+ const encodedKey = key
79
+ .split("/")
80
+ .map((part) => encodeURIComponent(part))
81
+ .join("/");
82
+ return `${base}/${encodedKey}`;
83
+ }
84
+ function assertInsideRoot(root, filePath) {
85
+ const pathFromRoot = relative(root, filePath);
86
+ if (pathFromRoot === "" ||
87
+ pathFromRoot.startsWith("..") ||
88
+ isAbsolute(pathFromRoot)) {
89
+ throw new Error("Storage key resolved outside the storage root.");
90
+ }
91
+ }
92
+ function objectPath(root, key) {
93
+ validateStorageKey(key);
94
+ const filePath = resolve(root, ...key.split("/"));
95
+ assertInsideRoot(root, filePath);
96
+ return filePath;
97
+ }
98
+ function metadataPath(root, key) {
99
+ validateStorageKey(key);
100
+ const filePath = `${resolve(root, metadataRootName, ...key.split("/"))}.json`;
101
+ assertInsideRoot(root, filePath);
102
+ return filePath;
103
+ }
104
+ async function fileExists(filePath) {
105
+ try {
106
+ const fileStat = await statFile(filePath);
107
+ return fileStat.isFile();
108
+ }
109
+ catch (error) {
110
+ if (error.code === "ENOENT")
111
+ return false;
112
+ throw error;
113
+ }
114
+ }
115
+ async function readMetadata(root, key) {
116
+ const filePath = metadataPath(root, key);
117
+ try {
118
+ const raw = await readFile(filePath, "utf8");
119
+ return JSON.parse(raw);
120
+ }
121
+ catch (error) {
122
+ if (error.code === "ENOENT")
123
+ return null;
124
+ throw error;
125
+ }
126
+ }
127
+ async function writeMetadata(filePath, metadata) {
128
+ await writeFile(filePath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
129
+ }
130
+ function temporaryPath(filePath, purpose) {
131
+ return `${filePath}.${purpose}.${randomUUID()}`;
132
+ }
133
+ async function renameFileIfExists(from, to) {
134
+ try {
135
+ const current = await statFile(from);
136
+ if (!current.isFile()) {
137
+ throw new Error(`Storage path is not a file: ${from}`);
138
+ }
139
+ }
140
+ catch (error) {
141
+ if (error.code === "ENOENT")
142
+ return false;
143
+ throw error;
144
+ }
145
+ await rename(from, to);
146
+ return true;
147
+ }
148
+ async function cleanupFile(filePath) {
149
+ await rm(filePath, { force: true });
150
+ }
151
+ async function restoreBackup(backupPath, filePath, backedUp) {
152
+ if (!backedUp)
153
+ return;
154
+ await rm(filePath, { force: true });
155
+ await rename(backupPath, filePath);
156
+ }
157
+ async function writeObjectAndMetadata(filePath, metaPath, body, metadata) {
158
+ const tempFilePath = temporaryPath(filePath, "tmp");
159
+ const tempMetaPath = temporaryPath(metaPath, "tmp");
160
+ const backupFilePath = temporaryPath(filePath, "backup");
161
+ const backupMetaPath = temporaryPath(metaPath, "backup");
162
+ let objectBackedUp = false;
163
+ let metadataBackedUp = false;
164
+ let objectCommitted = false;
165
+ let metadataCommitted = false;
166
+ try {
167
+ await Promise.all([
168
+ mkdir(dirname(filePath), { recursive: true }),
169
+ mkdir(dirname(metaPath), { recursive: true }),
170
+ ]);
171
+ await writeStorageBody(tempFilePath, body);
172
+ const objectStat = await statFile(tempFilePath);
173
+ if (!objectStat.isFile()) {
174
+ throw new Error("Storage body did not produce a file.");
175
+ }
176
+ await writeMetadata(tempMetaPath, metadata);
177
+ objectBackedUp = await renameFileIfExists(filePath, backupFilePath);
178
+ metadataBackedUp = await renameFileIfExists(metaPath, backupMetaPath);
179
+ await rename(tempFilePath, filePath);
180
+ objectCommitted = true;
181
+ await rename(tempMetaPath, metaPath);
182
+ metadataCommitted = true;
183
+ await Promise.all([
184
+ cleanupFile(backupFilePath),
185
+ cleanupFile(backupMetaPath),
186
+ ]);
187
+ return objectStat.size;
188
+ }
189
+ catch (error) {
190
+ await Promise.allSettled([
191
+ cleanupFile(tempFilePath),
192
+ cleanupFile(tempMetaPath),
193
+ objectCommitted ? cleanupFile(filePath) : Promise.resolve(),
194
+ metadataCommitted ? cleanupFile(metaPath) : Promise.resolve(),
195
+ ]);
196
+ await Promise.allSettled([
197
+ restoreBackup(backupFilePath, filePath, objectBackedUp),
198
+ restoreBackup(backupMetaPath, metaPath, metadataBackedUp),
199
+ ]);
200
+ throw error;
201
+ }
202
+ }
203
+ function metadataToObject(key, size, fallbackLastModified, metadata) {
204
+ return {
205
+ key,
206
+ size,
207
+ ...(metadata?.contentType !== undefined
208
+ ? { contentType: metadata.contentType }
209
+ : {}),
210
+ ...(metadata?.cacheControl !== undefined
211
+ ? { cacheControl: metadata.cacheControl }
212
+ : {}),
213
+ metadata: { ...(metadata?.metadata ?? {}) },
214
+ visibility: metadata?.visibility ?? "private",
215
+ lastModified: metadata?.lastModified
216
+ ? new Date(metadata.lastModified)
217
+ : fallbackLastModified,
218
+ };
219
+ }
220
+ function createObjectBody(object, filePath) {
221
+ let bodyUsed = false;
222
+ function markConsumed() {
223
+ if (bodyUsed) {
224
+ throw new Error("Storage object body has already been consumed.");
225
+ }
226
+ bodyUsed = true;
227
+ }
228
+ return {
229
+ ...object,
230
+ get bodyUsed() {
231
+ return bodyUsed;
232
+ },
233
+ stream() {
234
+ markConsumed();
235
+ return Readable.toWeb(createReadStream(filePath));
236
+ },
237
+ async bytes() {
238
+ markConsumed();
239
+ return copyBytes(await readFile(filePath));
240
+ },
241
+ async arrayBuffer() {
242
+ markConsumed();
243
+ return bytesToArrayBuffer(await readFile(filePath));
244
+ },
245
+ async text() {
246
+ markConsumed();
247
+ return readFile(filePath, "utf8");
248
+ },
249
+ };
250
+ }
251
+ function errorMessage(error) {
252
+ return error instanceof Error ? error.message : String(error);
253
+ }
254
+ export function createLocalStorage(options) {
255
+ const root = resolve(options.root);
256
+ const instrumentation = createProviderInstrumentation(options.instrumentation, {
257
+ providerName: "storage-local",
258
+ watcher: "storage",
259
+ });
260
+ const recordStorageEvent = (event) => {
261
+ instrumentation.custom({
262
+ name: `storage.${event.operation}`,
263
+ label: `Storage ${event.operation}`,
264
+ summary: event.summary,
265
+ details: {
266
+ operation: event.operation,
267
+ key: event.key,
268
+ ...event.details,
269
+ },
270
+ });
271
+ };
272
+ async function statObject(key) {
273
+ const filePath = objectPath(root, key);
274
+ try {
275
+ const [fileStat, metadata] = await Promise.all([
276
+ statFile(filePath),
277
+ readMetadata(root, key),
278
+ ]);
279
+ if (!fileStat.isFile())
280
+ return null;
281
+ return metadataToObject(key, fileStat.size, fileStat.mtime, metadata);
282
+ }
283
+ catch (error) {
284
+ if (error.code === "ENOENT")
285
+ return null;
286
+ throw error;
287
+ }
288
+ }
289
+ return {
290
+ async put(key, body, putOptions) {
291
+ const startedAt = Date.now();
292
+ try {
293
+ const filePath = objectPath(root, key);
294
+ const metaPath = metadataPath(root, key);
295
+ const lastModified = new Date();
296
+ const metadata = {
297
+ ...(putOptions?.contentType !== undefined
298
+ ? { contentType: putOptions.contentType }
299
+ : {}),
300
+ ...(putOptions?.cacheControl !== undefined
301
+ ? { cacheControl: putOptions.cacheControl }
302
+ : {}),
303
+ metadata: { ...(putOptions?.metadata ?? {}) },
304
+ visibility: putOptions?.visibility ?? "private",
305
+ lastModified: lastModified.toISOString(),
306
+ };
307
+ const size = await writeObjectAndMetadata(filePath, metaPath, body, metadata);
308
+ const object = metadataToObject(key, size, lastModified, metadata);
309
+ recordStorageEvent({
310
+ operation: "put",
311
+ key,
312
+ summary: "Storage object written",
313
+ details: {
314
+ size: object.size,
315
+ visibility: object.visibility,
316
+ contentType: object.contentType ?? null,
317
+ metadataKeys: Object.keys(object.metadata),
318
+ durationMs: Date.now() - startedAt,
319
+ },
320
+ });
321
+ return object;
322
+ }
323
+ catch (error) {
324
+ recordStorageEvent({
325
+ operation: "put.failed",
326
+ key,
327
+ summary: "Storage put failed",
328
+ details: {
329
+ durationMs: Date.now() - startedAt,
330
+ error: errorMessage(error),
331
+ },
332
+ });
333
+ throw error;
334
+ }
335
+ },
336
+ async get(key) {
337
+ const startedAt = Date.now();
338
+ try {
339
+ const object = await statObject(key);
340
+ const filePath = objectPath(root, key);
341
+ recordStorageEvent({
342
+ operation: "get",
343
+ key,
344
+ summary: object ? "Storage object read" : "Storage object missing",
345
+ details: {
346
+ hit: object !== null,
347
+ size: object?.size ?? null,
348
+ durationMs: Date.now() - startedAt,
349
+ },
350
+ });
351
+ if (!object)
352
+ return null;
353
+ return createObjectBody(object, filePath);
354
+ }
355
+ catch (error) {
356
+ recordStorageEvent({
357
+ operation: "get.failed",
358
+ key,
359
+ summary: "Storage get failed",
360
+ details: {
361
+ durationMs: Date.now() - startedAt,
362
+ error: errorMessage(error),
363
+ },
364
+ });
365
+ throw error;
366
+ }
367
+ },
368
+ async stat(key) {
369
+ const startedAt = Date.now();
370
+ try {
371
+ const object = await statObject(key);
372
+ recordStorageEvent({
373
+ operation: "stat",
374
+ key,
375
+ summary: object ? "Storage object found" : "Storage object missing",
376
+ details: {
377
+ hit: object !== null,
378
+ size: object?.size ?? null,
379
+ durationMs: Date.now() - startedAt,
380
+ },
381
+ });
382
+ return object;
383
+ }
384
+ catch (error) {
385
+ recordStorageEvent({
386
+ operation: "stat.failed",
387
+ key,
388
+ summary: "Storage stat failed",
389
+ details: {
390
+ durationMs: Date.now() - startedAt,
391
+ error: errorMessage(error),
392
+ },
393
+ });
394
+ throw error;
395
+ }
396
+ },
397
+ async delete(key) {
398
+ const startedAt = Date.now();
399
+ try {
400
+ const filePath = objectPath(root, key);
401
+ const metaPath = metadataPath(root, key);
402
+ const existed = await fileExists(filePath);
403
+ await Promise.all([
404
+ existed ? rm(filePath, { force: true }) : Promise.resolve(),
405
+ rm(metaPath, { force: true }),
406
+ ]);
407
+ recordStorageEvent({
408
+ operation: "delete",
409
+ key,
410
+ summary: existed
411
+ ? "Storage object deleted"
412
+ : "Storage object missing",
413
+ details: {
414
+ deleted: existed,
415
+ durationMs: Date.now() - startedAt,
416
+ },
417
+ });
418
+ return existed;
419
+ }
420
+ catch (error) {
421
+ recordStorageEvent({
422
+ operation: "delete.failed",
423
+ key,
424
+ summary: "Storage delete failed",
425
+ details: {
426
+ durationMs: Date.now() - startedAt,
427
+ error: errorMessage(error),
428
+ },
429
+ });
430
+ throw error;
431
+ }
432
+ },
433
+ async exists(key) {
434
+ const startedAt = Date.now();
435
+ try {
436
+ const exists = await fileExists(objectPath(root, key));
437
+ recordStorageEvent({
438
+ operation: "exists",
439
+ key,
440
+ summary: exists ? "Storage object exists" : "Storage object missing",
441
+ details: {
442
+ exists,
443
+ durationMs: Date.now() - startedAt,
444
+ },
445
+ });
446
+ return exists;
447
+ }
448
+ catch (error) {
449
+ recordStorageEvent({
450
+ operation: "exists.failed",
451
+ key,
452
+ summary: "Storage exists failed",
453
+ details: {
454
+ durationMs: Date.now() - startedAt,
455
+ error: errorMessage(error),
456
+ },
457
+ });
458
+ throw error;
459
+ }
460
+ },
461
+ async publicUrl(key) {
462
+ const startedAt = Date.now();
463
+ try {
464
+ const object = await statObject(key);
465
+ const url = object?.visibility === "public" && options.publicBaseUrl
466
+ ? joinPublicUrl(options.publicBaseUrl, key)
467
+ : null;
468
+ recordStorageEvent({
469
+ operation: "publicUrl",
470
+ key,
471
+ summary: url ? "Storage public URL resolved" : "No public URL",
472
+ details: {
473
+ hit: object !== null,
474
+ visibility: object?.visibility ?? null,
475
+ hasPublicUrl: url !== null,
476
+ durationMs: Date.now() - startedAt,
477
+ },
478
+ });
479
+ return url;
480
+ }
481
+ catch (error) {
482
+ recordStorageEvent({
483
+ operation: "publicUrl.failed",
484
+ key,
485
+ summary: "Storage public URL failed",
486
+ details: {
487
+ durationMs: Date.now() - startedAt,
488
+ error: errorMessage(error),
489
+ },
490
+ });
491
+ throw error;
492
+ }
493
+ },
494
+ };
495
+ }
496
+ export function createLocalStorageProvider(options = {}) {
497
+ const ConfigSchema = LocalStorageConfigSchema.extend({
498
+ ROOT: z
499
+ .string()
500
+ .min(1)
501
+ .default(options.root ?? "storage/app"),
502
+ });
503
+ return createProvider({
504
+ name: options.name ?? "storage-local",
505
+ config: {
506
+ schema: ConfigSchema,
507
+ envPrefix: "STORAGE_",
508
+ },
509
+ async setup({ ports, config }) {
510
+ if (!config) {
511
+ throw new Error("[localStorageProvider] Missing storage config. " +
512
+ "Please set STORAGE_ROOT or use the default storage/app path.");
513
+ }
514
+ return {
515
+ ports: {
516
+ storage: createLocalStorage({
517
+ root: config.ROOT,
518
+ publicBaseUrl: config.PUBLIC_BASE_URL ?? options.publicBaseUrl,
519
+ instrumentation: ports,
520
+ }),
521
+ },
522
+ };
523
+ },
524
+ });
525
+ }
526
+ export const localStorageProvider = createLocalStorageProvider();
527
+ //# sourceMappingURL=index.js.map