@miguelmorales13/nestkit 0.4.1 → 0.5.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.
@@ -0,0 +1,528 @@
1
+ import {
2
+ __decorateClass
3
+ } from "../chunk-4MGIQFAJ.js";
4
+
5
+ // src/storage/storage.port.ts
6
+ var STORAGE = /* @__PURE__ */ Symbol("STORAGE");
7
+
8
+ // src/storage/storage.options.ts
9
+ var STORAGE_OPTIONS = /* @__PURE__ */ Symbol("STORAGE_OPTIONS");
10
+
11
+ // src/storage/storage.module.ts
12
+ import { Global, Module } from "@nestjs/common";
13
+
14
+ // src/storage/adapters/local.storage.ts
15
+ import { createReadStream } from "fs";
16
+ import { mkdir, readFile, rm, stat, writeFile, readdir } from "fs/promises";
17
+ import { dirname, join, normalize, resolve, sep, relative } from "path";
18
+ import { pipeline } from "stream/promises";
19
+ import { createWriteStream } from "fs";
20
+ var LocalStorage = class {
21
+ constructor(options) {
22
+ this.root = resolve(options.root);
23
+ this.publicBaseUrl = options.publicBaseUrl?.replace(/\/$/, "");
24
+ }
25
+ /** Resolve a key to an absolute path, refusing anything that escapes the root. */
26
+ pathOf(key) {
27
+ const target = resolve(join(this.root, normalize(key)));
28
+ if (target !== this.root && !target.startsWith(this.root + sep)) {
29
+ throw new Error(`LocalStorage: key escapes the storage root: ${key}`);
30
+ }
31
+ return target;
32
+ }
33
+ async put(key, data) {
34
+ const path = this.pathOf(key);
35
+ await mkdir(dirname(path), { recursive: true });
36
+ await writeFile(path, data);
37
+ return { key, size: data.byteLength };
38
+ }
39
+ async putStream(key, stream) {
40
+ const path = this.pathOf(key);
41
+ await mkdir(dirname(path), { recursive: true });
42
+ await pipeline(stream, createWriteStream(path));
43
+ const s = await stat(path);
44
+ return { key, size: s.size, lastModified: s.mtime };
45
+ }
46
+ async get(key) {
47
+ return readFile(this.pathOf(key));
48
+ }
49
+ async getStream(key, range) {
50
+ const path = this.pathOf(key);
51
+ const s = await stat(path);
52
+ if (!range) {
53
+ return { stream: createReadStream(path), contentLength: s.size, totalSize: s.size, lastModified: s.mtime };
54
+ }
55
+ const end = range.end ?? s.size - 1;
56
+ return {
57
+ stream: createReadStream(path, { start: range.start, end }),
58
+ contentLength: end - range.start + 1,
59
+ totalSize: s.size,
60
+ contentRange: `bytes ${range.start}-${end}/${s.size}`,
61
+ lastModified: s.mtime
62
+ };
63
+ }
64
+ async stat(key) {
65
+ try {
66
+ const s = await stat(this.pathOf(key));
67
+ return { key, size: s.size, lastModified: s.mtime };
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ async exists(key) {
73
+ return await this.stat(key) !== null;
74
+ }
75
+ async delete(key) {
76
+ await rm(this.pathOf(key), { force: true });
77
+ }
78
+ async list(prefix, options) {
79
+ const base = this.pathOf(prefix);
80
+ const objects = [];
81
+ const walk = async (dir) => {
82
+ let entries;
83
+ try {
84
+ entries = await readdir(dir, { withFileTypes: true });
85
+ } catch {
86
+ return;
87
+ }
88
+ for (const entry of entries) {
89
+ if (options?.limit && objects.length >= options.limit) return;
90
+ const full = join(dir, String(entry.name));
91
+ if (entry.isDirectory()) await walk(full);
92
+ else {
93
+ const s = await stat(full);
94
+ objects.push({ key: relative(this.root, full).split(sep).join("/"), size: s.size, lastModified: s.mtime });
95
+ }
96
+ }
97
+ };
98
+ await walk(base);
99
+ return { objects };
100
+ }
101
+ publicUrl(key) {
102
+ return this.publicBaseUrl ? `${this.publicBaseUrl}/${key}` : `/${key}`;
103
+ }
104
+ async signedReadUrl(key) {
105
+ return this.publicUrl(key) ?? `/${key}`;
106
+ }
107
+ async signedUploadUrl() {
108
+ throw new Error(
109
+ "LocalStorage does not support direct signed uploads \u2014 a client cannot PUT to the server disk. Upload through your API in dev, or use the S3/R2 or Bunny adapter."
110
+ );
111
+ }
112
+ };
113
+
114
+ // src/storage/adapters/s3.storage.ts
115
+ var S3Storage = class {
116
+ constructor(options) {
117
+ this.bucket = options.bucket;
118
+ this.publicBaseUrl = options.publicBaseUrl?.replace(/\/$/, "");
119
+ this.config = {
120
+ region: options.region ?? "us-east-1",
121
+ endpoint: options.endpoint,
122
+ forcePathStyle: options.forcePathStyle,
123
+ credentials: {
124
+ accessKeyId: options.accessKeyId,
125
+ secretAccessKey: options.secretAccessKey
126
+ }
127
+ };
128
+ }
129
+ /** Lazily construct (and memoize) the S3 client, loading the SDK on first use. */
130
+ async client() {
131
+ if (!this.clientPromise) {
132
+ this.clientPromise = import("@aws-sdk/client-s3").then(
133
+ ({ S3Client }) => new S3Client(this.config)
134
+ );
135
+ }
136
+ return this.clientPromise;
137
+ }
138
+ async put(key, data, options) {
139
+ const [client, { PutObjectCommand }] = await Promise.all([
140
+ this.client(),
141
+ import("@aws-sdk/client-s3")
142
+ ]);
143
+ const out = await client.send(
144
+ new PutObjectCommand({
145
+ Bucket: this.bucket,
146
+ Key: key,
147
+ Body: data,
148
+ ContentType: options?.contentType,
149
+ CacheControl: options?.cacheControl,
150
+ ContentDisposition: options?.contentDisposition,
151
+ Metadata: options?.metadata
152
+ })
153
+ );
154
+ return { key, size: data.byteLength, contentType: options?.contentType, etag: out.ETag };
155
+ }
156
+ async putStream(key, stream, options) {
157
+ const [client, { Upload }] = await Promise.all([this.client(), import("@aws-sdk/lib-storage")]);
158
+ const upload = new Upload({
159
+ client,
160
+ params: {
161
+ Bucket: this.bucket,
162
+ Key: key,
163
+ Body: stream,
164
+ ContentType: options?.contentType,
165
+ CacheControl: options?.cacheControl,
166
+ ContentDisposition: options?.contentDisposition,
167
+ Metadata: options?.metadata
168
+ }
169
+ });
170
+ const out = await upload.done();
171
+ const info = await this.stat(key);
172
+ return { key, size: info?.size ?? 0, contentType: options?.contentType, etag: out?.ETag };
173
+ }
174
+ async get(key) {
175
+ const { stream } = await this.getStream(key);
176
+ const chunks = [];
177
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
178
+ return Buffer.concat(chunks);
179
+ }
180
+ async getStream(key, range) {
181
+ const [client, { GetObjectCommand }] = await Promise.all([
182
+ this.client(),
183
+ import("@aws-sdk/client-s3")
184
+ ]);
185
+ const out = await client.send(
186
+ new GetObjectCommand({
187
+ Bucket: this.bucket,
188
+ Key: key,
189
+ Range: range ? `bytes=${range.start}-${range.end ?? ""}` : void 0
190
+ })
191
+ );
192
+ return {
193
+ stream: out.Body,
194
+ contentType: out.ContentType,
195
+ contentLength: out.ContentLength,
196
+ contentRange: out.ContentRange,
197
+ totalSize: out.ContentRange ? Number(out.ContentRange.split("/")[1]) : out.ContentLength,
198
+ lastModified: out.LastModified,
199
+ etag: out.ETag
200
+ };
201
+ }
202
+ async stat(key) {
203
+ const [client, { HeadObjectCommand }] = await Promise.all([
204
+ this.client(),
205
+ import("@aws-sdk/client-s3")
206
+ ]);
207
+ try {
208
+ const out = await client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
209
+ return {
210
+ key,
211
+ size: out.ContentLength ?? 0,
212
+ contentType: out.ContentType,
213
+ lastModified: out.LastModified,
214
+ etag: out.ETag
215
+ };
216
+ } catch (err) {
217
+ if (err?.$metadata?.httpStatusCode === 404 || err?.name === "NotFound") return null;
218
+ throw err;
219
+ }
220
+ }
221
+ async exists(key) {
222
+ return await this.stat(key) !== null;
223
+ }
224
+ async delete(key) {
225
+ const [client, { DeleteObjectCommand }] = await Promise.all([
226
+ this.client(),
227
+ import("@aws-sdk/client-s3")
228
+ ]);
229
+ await client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
230
+ }
231
+ async list(prefix, options) {
232
+ const [client, { ListObjectsV2Command }] = await Promise.all([
233
+ this.client(),
234
+ import("@aws-sdk/client-s3")
235
+ ]);
236
+ const out = await client.send(
237
+ new ListObjectsV2Command({
238
+ Bucket: this.bucket,
239
+ Prefix: prefix,
240
+ MaxKeys: options?.limit,
241
+ ContinuationToken: options?.cursor
242
+ })
243
+ );
244
+ return {
245
+ objects: (out.Contents ?? []).map((o) => ({
246
+ key: o.Key,
247
+ size: o.Size ?? 0,
248
+ lastModified: o.LastModified,
249
+ etag: o.ETag
250
+ })),
251
+ cursor: out.NextContinuationToken
252
+ };
253
+ }
254
+ async signedReadUrl(key, ttlSeconds = 3600) {
255
+ const [client, { GetObjectCommand }, { getSignedUrl }] = await Promise.all([
256
+ this.client(),
257
+ import("@aws-sdk/client-s3"),
258
+ import("@aws-sdk/s3-request-presigner")
259
+ ]);
260
+ return getSignedUrl(client, new GetObjectCommand({ Bucket: this.bucket, Key: key }), {
261
+ expiresIn: ttlSeconds
262
+ });
263
+ }
264
+ async signedUploadUrl(key, options) {
265
+ const [client, { PutObjectCommand }, { getSignedUrl }] = await Promise.all([
266
+ this.client(),
267
+ import("@aws-sdk/client-s3"),
268
+ import("@aws-sdk/s3-request-presigner")
269
+ ]);
270
+ const ttl = options?.ttlSeconds ?? 900;
271
+ const url = await getSignedUrl(
272
+ client,
273
+ new PutObjectCommand({
274
+ Bucket: this.bucket,
275
+ Key: key,
276
+ ContentType: options?.contentType,
277
+ CacheControl: options?.cacheControl
278
+ }),
279
+ { expiresIn: ttl }
280
+ );
281
+ return {
282
+ url,
283
+ method: "PUT",
284
+ headers: options?.contentType ? { "Content-Type": options.contentType } : void 0,
285
+ key,
286
+ expiresAt: new Date(Date.now() + ttl * 1e3)
287
+ };
288
+ }
289
+ publicUrl(key) {
290
+ return this.publicBaseUrl ? `${this.publicBaseUrl}/${key}` : null;
291
+ }
292
+ };
293
+
294
+ // src/storage/adapters/bunny.storage.ts
295
+ import { createHash } from "crypto";
296
+ import { Readable } from "stream";
297
+ var BunnyStorage = class {
298
+ constructor(options) {
299
+ const host = `${options.region ? `${options.region}.` : ""}storage.bunnycdn.com`;
300
+ this.base = `https://${host}/${options.storageZone}`;
301
+ this.apiKey = options.apiKey;
302
+ this.pullZoneUrl = options.pullZoneUrl?.replace(/\/$/, "");
303
+ this.tokenSecurityKey = options.tokenSecurityKey;
304
+ }
305
+ url(key) {
306
+ return `${this.base}/${key.split("/").map(encodeURIComponent).join("/")}`;
307
+ }
308
+ async put(key, data, options) {
309
+ const res = await fetch(this.url(key), {
310
+ method: "PUT",
311
+ headers: {
312
+ AccessKey: this.apiKey,
313
+ "Content-Type": options?.contentType ?? "application/octet-stream"
314
+ },
315
+ body: data
316
+ });
317
+ if (!res.ok) throw new Error(`Bunny put ${key} failed: ${res.status} ${await res.text()}`);
318
+ return { key, size: data.byteLength, contentType: options?.contentType };
319
+ }
320
+ async putStream(key, stream, options) {
321
+ const res = await fetch(this.url(key), {
322
+ method: "PUT",
323
+ headers: {
324
+ AccessKey: this.apiKey,
325
+ "Content-Type": options?.contentType ?? "application/octet-stream"
326
+ },
327
+ body: Readable.toWeb(stream),
328
+ // Required by fetch when the body is a stream.
329
+ duplex: "half"
330
+ });
331
+ if (!res.ok) throw new Error(`Bunny putStream ${key} failed: ${res.status} ${await res.text()}`);
332
+ const info = await this.stat(key);
333
+ return { key, size: info?.size ?? options?.contentLength ?? 0, contentType: options?.contentType };
334
+ }
335
+ async get(key) {
336
+ const res = await fetch(this.url(key), { headers: { AccessKey: this.apiKey } });
337
+ if (!res.ok) throw new Error(`Bunny get ${key} failed: ${res.status}`);
338
+ return Buffer.from(await res.arrayBuffer());
339
+ }
340
+ async getStream(key, range) {
341
+ const headers = { AccessKey: this.apiKey };
342
+ if (range) headers.Range = `bytes=${range.start}-${range.end ?? ""}`;
343
+ const res = await fetch(this.url(key), { headers });
344
+ if (!res.ok && res.status !== 206) throw new Error(`Bunny getStream ${key} failed: ${res.status}`);
345
+ if (!res.body) throw new Error(`Bunny getStream ${key}: empty body`);
346
+ const contentLength = Number(res.headers.get("content-length")) || void 0;
347
+ const contentRange = res.headers.get("content-range") ?? void 0;
348
+ return {
349
+ stream: Readable.fromWeb(res.body),
350
+ contentType: res.headers.get("content-type") ?? void 0,
351
+ contentLength,
352
+ contentRange,
353
+ totalSize: contentRange ? Number(contentRange.split("/")[1]) : contentLength
354
+ };
355
+ }
356
+ async stat(key) {
357
+ const slash = key.lastIndexOf("/");
358
+ const dir = slash >= 0 ? key.slice(0, slash) : "";
359
+ const name = slash >= 0 ? key.slice(slash + 1) : key;
360
+ const entries = await this.listRaw(dir);
361
+ const found = entries.find((e) => !e.IsDirectory && e.ObjectName === name);
362
+ if (!found) return null;
363
+ return {
364
+ key,
365
+ size: found.Length,
366
+ contentType: found.ContentType || void 0,
367
+ lastModified: found.LastChanged ? new Date(found.LastChanged) : void 0
368
+ };
369
+ }
370
+ async exists(key) {
371
+ return await this.stat(key) !== null;
372
+ }
373
+ async delete(key) {
374
+ const res = await fetch(this.url(key), { method: "DELETE", headers: { AccessKey: this.apiKey } });
375
+ if (!res.ok && res.status !== 404) {
376
+ throw new Error(`Bunny delete ${key} failed: ${res.status} ${await res.text()}`);
377
+ }
378
+ }
379
+ async listRaw(prefix) {
380
+ const path = prefix ? `${prefix.replace(/\/$/, "")}/` : "";
381
+ const res = await fetch(`${this.base}/${path}`, { headers: { AccessKey: this.apiKey } });
382
+ if (res.status === 404) return [];
383
+ if (!res.ok) throw new Error(`Bunny list ${prefix} failed: ${res.status}`);
384
+ return await res.json();
385
+ }
386
+ async list(prefix, options) {
387
+ const entries = await this.listRaw(prefix);
388
+ const base = prefix ? `${prefix.replace(/\/$/, "")}/` : "";
389
+ let objects = entries.filter((e) => !e.IsDirectory).map((e) => ({
390
+ key: `${base}${e.ObjectName}`,
391
+ size: e.Length,
392
+ contentType: e.ContentType || void 0,
393
+ lastModified: e.LastChanged ? new Date(e.LastChanged) : void 0
394
+ }));
395
+ if (options?.limit) objects = objects.slice(0, options.limit);
396
+ return { objects };
397
+ }
398
+ publicUrl(key) {
399
+ if (!this.pullZoneUrl) return null;
400
+ return `${this.pullZoneUrl}/${key.split("/").map(encodeURIComponent).join("/")}`;
401
+ }
402
+ async signedReadUrl(key, ttlSeconds = 3600) {
403
+ if (!this.pullZoneUrl) {
404
+ throw new Error("BunnyStorage.signedReadUrl: set pullZoneUrl (Bunny signs Pull Zone URLs).");
405
+ }
406
+ if (!this.tokenSecurityKey) {
407
+ return this.publicUrl(key);
408
+ }
409
+ const path = `/${key.split("/").map(encodeURIComponent).join("/")}`;
410
+ const expires = Math.floor(Date.now() / 1e3) + ttlSeconds;
411
+ const token = createHash("sha256").update(this.tokenSecurityKey + path + expires).digest("base64").replace(/\n/g, "").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
412
+ return `${this.pullZoneUrl}${path}?token=${token}&expires=${expires}`;
413
+ }
414
+ async signedUploadUrl() {
415
+ throw new Error(
416
+ "BunnyStorage does not support presigned direct uploads (Storage has no PUT signature that hides the zone password). Upload through your API, or use Bunny Stream/TUS for large video."
417
+ );
418
+ }
419
+ };
420
+
421
+ // src/storage/storage.module.ts
422
+ var env = (k) => process.env[k] || void 0;
423
+ function r2ToS3Options(r2) {
424
+ return {
425
+ bucket: r2.bucket,
426
+ region: "auto",
427
+ endpoint: `https://${r2.accountId}.r2.cloudflarestorage.com`,
428
+ accessKeyId: r2.accessKeyId,
429
+ secretAccessKey: r2.secretAccessKey,
430
+ publicBaseUrl: r2.publicBaseUrl
431
+ };
432
+ }
433
+ function resolveStorageOptions(options = {}) {
434
+ const provider = options.provider ?? env("STORAGE_PROVIDER") ?? "local";
435
+ return {
436
+ provider,
437
+ local: options.local ?? {
438
+ root: env("STORAGE_LOCAL_ROOT") ?? ".storage",
439
+ publicBaseUrl: env("STORAGE_PUBLIC_BASE_URL")
440
+ },
441
+ s3: options.s3 ?? (env("S3_BUCKET") ? {
442
+ bucket: env("S3_BUCKET"),
443
+ region: env("S3_REGION"),
444
+ endpoint: env("S3_ENDPOINT"),
445
+ accessKeyId: env("S3_ACCESS_KEY_ID") ?? "",
446
+ secretAccessKey: env("S3_SECRET_ACCESS_KEY") ?? "",
447
+ publicBaseUrl: env("S3_PUBLIC_BASE_URL"),
448
+ forcePathStyle: env("S3_FORCE_PATH_STYLE") === "true"
449
+ } : void 0),
450
+ r2: options.r2 ?? (env("R2_ACCOUNT_ID") ? {
451
+ accountId: env("R2_ACCOUNT_ID"),
452
+ bucket: env("R2_BUCKET") ?? "",
453
+ accessKeyId: env("R2_ACCESS_KEY_ID") ?? "",
454
+ secretAccessKey: env("R2_SECRET_ACCESS_KEY") ?? "",
455
+ publicBaseUrl: env("R2_PUBLIC_BASE_URL")
456
+ } : void 0),
457
+ bunny: options.bunny ?? (env("BUNNY_STORAGE_ZONE") ? {
458
+ storageZone: env("BUNNY_STORAGE_ZONE"),
459
+ apiKey: env("BUNNY_API_KEY") ?? "",
460
+ region: env("BUNNY_REGION"),
461
+ pullZoneUrl: env("BUNNY_PULL_ZONE_URL"),
462
+ tokenSecurityKey: env("BUNNY_TOKEN_SECURITY_KEY")
463
+ } : void 0)
464
+ };
465
+ }
466
+ function createStorageAdapter(options) {
467
+ const resolved = resolveStorageOptions(options);
468
+ switch (resolved.provider) {
469
+ case "local":
470
+ return new LocalStorage(resolved.local);
471
+ case "s3":
472
+ if (!resolved.s3) throw new Error('StorageModule: provider "s3" selected but no s3 options / S3_* env set.');
473
+ return new S3Storage(resolved.s3);
474
+ case "r2":
475
+ if (!resolved.r2) throw new Error('StorageModule: provider "r2" selected but no r2 options / R2_* env set.');
476
+ return new S3Storage(r2ToS3Options(resolved.r2));
477
+ case "bunny":
478
+ if (!resolved.bunny)
479
+ throw new Error('StorageModule: provider "bunny" selected but no bunny options / BUNNY_* env set.');
480
+ return new BunnyStorage(resolved.bunny);
481
+ default:
482
+ throw new Error(`StorageModule: unknown provider "${resolved.provider}".`);
483
+ }
484
+ }
485
+ var StorageModule = class {
486
+ static forRoot(options = {}) {
487
+ const providers = [
488
+ { provide: STORAGE_OPTIONS, useValue: options },
489
+ { provide: STORAGE, useFactory: () => createStorageAdapter(options) }
490
+ ];
491
+ return { module: StorageModule, providers, exports: [STORAGE, STORAGE_OPTIONS] };
492
+ }
493
+ static forRootAsync(config) {
494
+ const providers = [
495
+ {
496
+ provide: STORAGE_OPTIONS,
497
+ useFactory: config.useFactory,
498
+ inject: config.inject ?? []
499
+ },
500
+ {
501
+ provide: STORAGE,
502
+ useFactory: (options) => createStorageAdapter(options),
503
+ inject: [STORAGE_OPTIONS]
504
+ }
505
+ ];
506
+ return {
507
+ module: StorageModule,
508
+ imports: config.imports ?? [],
509
+ providers,
510
+ exports: [STORAGE, STORAGE_OPTIONS]
511
+ };
512
+ }
513
+ };
514
+ StorageModule = __decorateClass([
515
+ Global(),
516
+ Module({})
517
+ ], StorageModule);
518
+ export {
519
+ BunnyStorage,
520
+ LocalStorage,
521
+ S3Storage,
522
+ STORAGE,
523
+ STORAGE_OPTIONS,
524
+ StorageModule,
525
+ createStorageAdapter,
526
+ r2ToS3Options,
527
+ resolveStorageOptions
528
+ };
@@ -0,0 +1,47 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import { type StorageOptions, type S3StorageOptions } from './storage.options.js';
3
+ import { type StoragePort } from './storage.port.js';
4
+ /** Cloudflare R2 is S3-compatible: map its options onto an S3 client pointed at R2. */
5
+ export declare function r2ToS3Options(r2: {
6
+ accountId: string;
7
+ bucket: string;
8
+ accessKeyId: string;
9
+ secretAccessKey: string;
10
+ publicBaseUrl?: string;
11
+ }): S3StorageOptions;
12
+ /**
13
+ * Merge explicit options with environment variables. Anything not passed in code
14
+ * falls back to the env, so `StorageModule.forRoot()` with no arguments works when
15
+ * the environment is configured.
16
+ *
17
+ * Recognized vars: `STORAGE_PROVIDER`; local `STORAGE_LOCAL_ROOT`,
18
+ * `STORAGE_PUBLIC_BASE_URL`; s3 `S3_BUCKET`, `S3_REGION`, `S3_ENDPOINT`,
19
+ * `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_PUBLIC_BASE_URL`,
20
+ * `S3_FORCE_PATH_STYLE`; r2 `R2_ACCOUNT_ID`, `R2_BUCKET`, `R2_ACCESS_KEY_ID`,
21
+ * `R2_SECRET_ACCESS_KEY`, `R2_PUBLIC_BASE_URL`; bunny `BUNNY_STORAGE_ZONE`,
22
+ * `BUNNY_API_KEY`, `BUNNY_REGION`, `BUNNY_PULL_ZONE_URL`, `BUNNY_TOKEN_SECURITY_KEY`.
23
+ */
24
+ export declare function resolveStorageOptions(options?: StorageOptions): StorageOptions;
25
+ /** Construct the adapter the options select, failing loudly on missing config. */
26
+ export declare function createStorageAdapter(options: StorageOptions): StoragePort;
27
+ /**
28
+ * Provider-agnostic file storage for NestJS. Register once in the root module and
29
+ * inject the port anywhere with `@Inject(STORAGE) storage: StoragePort`.
30
+ *
31
+ * ```ts
32
+ * StorageModule.forRoot({ provider: 'r2' }) // reads R2_* from env
33
+ * StorageModule.forRoot({ provider: 'bunny', bunny: { storageZone, apiKey, pullZoneUrl } })
34
+ * StorageModule.forRootAsync({ inject: [Config], useFactory: (c) => ({ provider: c.storageProvider }) })
35
+ * ```
36
+ *
37
+ * Switching providers is this one line — no call site changes, because everything
38
+ * depends on `StoragePort`, never on a concrete adapter.
39
+ */
40
+ export declare class StorageModule {
41
+ static forRoot(options?: StorageOptions): DynamicModule;
42
+ static forRootAsync(config: {
43
+ imports?: any[];
44
+ inject?: any[];
45
+ useFactory: (...args: any[]) => StorageOptions | Promise<StorageOptions>;
46
+ }): DynamicModule;
47
+ }
@@ -0,0 +1,79 @@
1
+ /** DI token carrying the resolved {@link StorageOptions}. */
2
+ export declare const STORAGE_OPTIONS: unique symbol;
3
+ /** Which adapter backs the {@link StoragePort}. */
4
+ export type StorageProvider = 'local' | 's3' | 'r2' | 'bunny';
5
+ /** Local filesystem adapter — for development. Not for production. */
6
+ export interface LocalStorageOptions {
7
+ /** Directory objects are written under. Created on demand. */
8
+ root: string;
9
+ /**
10
+ * Base URL that serves the files (including any global prefix), for `publicUrl`
11
+ * and `signedReadUrl`. Your app must actually serve `root` under this URL. If
12
+ * omitted, both return a `key`-relative path and the app resolves it.
13
+ */
14
+ publicBaseUrl?: string;
15
+ }
16
+ /** S3-compatible adapter. Covers AWS S3 and anything speaking the S3 API. */
17
+ export interface S3StorageOptions {
18
+ bucket: string;
19
+ region?: string;
20
+ /** Custom endpoint for non-AWS S3 (MinIO, R2, …). Omit for AWS S3. */
21
+ endpoint?: string;
22
+ accessKeyId: string;
23
+ secretAccessKey: string;
24
+ /** Path-style addressing (`endpoint/bucket/key`) instead of virtual-host. MinIO needs this. */
25
+ forcePathStyle?: boolean;
26
+ /** Public base URL (a CDN or the bucket's website endpoint) for `publicUrl`. Omit for a private bucket. */
27
+ publicBaseUrl?: string;
28
+ }
29
+ /**
30
+ * Cloudflare R2. R2 speaks the S3 API, so under the hood this becomes an S3
31
+ * adapter pointed at `https://<accountId>.r2.cloudflarestorage.com` with region
32
+ * `auto`. Give it your R2 access key pair (not a Cloudflare API token).
33
+ */
34
+ export interface R2StorageOptions {
35
+ accountId: string;
36
+ bucket: string;
37
+ accessKeyId: string;
38
+ secretAccessKey: string;
39
+ /** Your public R2.dev URL or a custom domain bound to the bucket, for `publicUrl`. */
40
+ publicBaseUrl?: string;
41
+ }
42
+ /**
43
+ * Bunny.net Storage. Objects live in a Storage Zone; they are served publicly
44
+ * through the Pull Zone bound to it.
45
+ */
46
+ export interface BunnyStorageOptions {
47
+ /** Storage Zone name. */
48
+ storageZone: string;
49
+ /** Storage Zone password (the "password" / access key of the zone). */
50
+ apiKey: string;
51
+ /**
52
+ * Storage region hostname prefix: `''` (Falkenstein/default), `'ny'`, `'la'`,
53
+ * `'sg'`, `'syd'`, `'br'`, `'jh'`, `'uk'`, `'se'`, … Matches where you created
54
+ * the zone. Wrong region → 401.
55
+ */
56
+ region?: string;
57
+ /**
58
+ * Pull Zone URL that serves the files publicly, e.g.
59
+ * `https://my-zone.b-cdn.net`. Required for `publicUrl` and for `signedReadUrl`
60
+ * (Bunny signs Pull Zone URLs, not Storage URLs).
61
+ */
62
+ pullZoneUrl?: string;
63
+ /** Pull Zone "URL Token Authentication" key, to sign `signedReadUrl`. */
64
+ tokenSecurityKey?: string;
65
+ }
66
+ /**
67
+ * Storage configuration. `provider` chooses the adapter; only the matching
68
+ * option block is required. Any field can come from the environment instead —
69
+ * see `resolveStorageOptions` in `storage.module.ts` for the variable names —
70
+ * so `StorageModule.forRoot()` with no arguments works when the env is set.
71
+ */
72
+ export interface StorageOptions {
73
+ /** Adapter to use. Falls back to the `STORAGE_PROVIDER` env var, then `'local'`. */
74
+ provider?: StorageProvider;
75
+ local?: LocalStorageOptions;
76
+ s3?: S3StorageOptions;
77
+ r2?: R2StorageOptions;
78
+ bunny?: BunnyStorageOptions;
79
+ }