@zucker-framework/storage 1.0.0 → 1.0.2

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.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ReadStream } from 'fs';
2
2
  import { OnModuleDestroy, Type, DynamicModule, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
3
+ import { ReadStream as ReadStream$1 } from 'node:fs';
3
4
 
4
5
  interface UploadOptions {
5
6
  contentType?: string;
@@ -424,10 +425,108 @@ declare class ZuckerStorageModule {
424
425
  private static createAsyncProviders;
425
426
  }
426
427
 
428
+ interface StorageUrlHydrationOptions {
429
+ /** The application must establish ownership before allowing a key to be signed. */
430
+ allowedStorageKeys: ReadonlySet<string>;
431
+ getDownloadUrl: (key: string) => Promise<string>;
432
+ storageKeyField: string;
433
+ urlFields: readonly string[];
434
+ }
435
+ /**
436
+ * Clone a JSON-like response graph and resolve only authorized object keys.
437
+ * Dates/class instances pass through. Signatures are deduplicated within this
438
+ * call, never cached between requests. A signing failure rejects the whole call;
439
+ * generated URLs never mutate the source graph that might later be persisted.
440
+ */
441
+ declare function hydrateStorageUrls<T>(value: T, options: StorageUrlHydrationOptions): Promise<T>;
442
+
443
+ /** Disabled storage preserves startup availability while all data operations remain explicit. */
444
+ declare class UnavailableFileManager implements FileManager {
445
+ readonly providerType: StorageProviderType;
446
+ private readonly message;
447
+ constructor(providerType: StorageProviderType, message?: string);
448
+ isConfigured(): boolean;
449
+ private unavailable;
450
+ upload(_buffer: Buffer, _key: string, _options?: UploadOptions): Promise<UploadResult>;
451
+ uploadStream(_stream: ReadStream$1, _key: string, _options?: UploadOptions): Promise<UploadResult>;
452
+ download(_key: string): Promise<{
453
+ stream: ReadStream$1;
454
+ size: number;
455
+ }>;
456
+ delete(_key: string): Promise<void>;
457
+ exists(_key: string): Promise<boolean>;
458
+ getPublicUrl(_key: string): Promise<string>;
459
+ getDownloadUrl(_key: string): Promise<string>;
460
+ list(_prefix: string): Promise<FileInfo[]>;
461
+ }
462
+
463
+ interface DownloadedStorageObject {
464
+ body: Buffer;
465
+ reportedSize: number;
466
+ }
467
+ interface BufferedObjectStore<TMimeType extends string = string> {
468
+ read(key: string): Promise<DownloadedStorageObject>;
469
+ exists(key: string): Promise<boolean>;
470
+ write(key: string, body: Buffer, options: {
471
+ contentType: TMimeType;
472
+ metadata: Record<string, string>;
473
+ private: true;
474
+ }): Promise<{
475
+ size: number;
476
+ }>;
477
+ }
478
+ type StorageTransferErrorCode = 'STORAGE_NOT_CONFIGURED' | 'OBJECT_SIZE_OUT_OF_BOUNDS' | 'SOURCE_SIZE_MISMATCH' | 'TARGET_SIZE_CONFLICT' | 'TARGET_MIME_CONFLICT' | 'TARGET_HASH_CONFLICT' | 'UPLOAD_SIZE_MISMATCH';
479
+ declare class StorageTransferError extends Error {
480
+ readonly code: StorageTransferErrorCode;
481
+ constructor(code: StorageTransferErrorCode);
482
+ }
483
+ type StorageTransferErrorFactory = (code: StorageTransferErrorCode) => Error;
484
+ interface VerifiedStorageMetadata<TMimeType extends string = string> {
485
+ sha256: string;
486
+ size: number;
487
+ mimeType: TMimeType;
488
+ }
489
+ /** Compare every byte via SHA-256, with explicit size and MIME conflict reasons. */
490
+ declare function verifyStoredObject<TMimeType extends string>(object: DownloadedStorageObject, expected: VerifiedStorageMetadata<TMimeType>, detectMimeType: (body: Buffer) => TMimeType, errorFactory?: StorageTransferErrorFactory): void;
491
+ interface VerifiedObjectTransferOptions<TMimeType extends string> {
492
+ source: DownloadedStorageObject;
493
+ target: BufferedObjectStore<TMimeType>;
494
+ destinationKey: string;
495
+ mode: 'dry-run' | 'apply';
496
+ allowUpload: boolean;
497
+ detectMimeType: (body: Buffer) => TMimeType;
498
+ errorFactory?: StorageTransferErrorFactory;
499
+ }
500
+ /**
501
+ * Copy a bounded source buffer into a private destination, verify existing data
502
+ * before reuse and round-trip every upload before reporting success. Dry-run and
503
+ * disallowed uploads never write. This does not delete sources or commit metadata.
504
+ */
505
+ declare function transferVerifiedObject<TMimeType extends string>(options: VerifiedObjectTransferOptions<TMimeType>): Promise<VerifiedStorageMetadata<TMimeType> & {
506
+ targetDisposition: 'planned' | 'uploaded' | 'reused';
507
+ }>;
508
+
509
+ /** Bounded in-memory transfer adapter over the existing FileManager contract. */
510
+ declare class BufferedFileManagerStore implements BufferedObjectStore {
511
+ private readonly manager;
512
+ private readonly maxObjectBytes;
513
+ private readonly errorFactory;
514
+ constructor(manager: FileManager, maxObjectBytes: number, errorFactory?: StorageTransferErrorFactory);
515
+ read(key: string): Promise<DownloadedStorageObject>;
516
+ exists(key: string): Promise<boolean>;
517
+ write(key: string, body: Buffer, options: {
518
+ contentType: string;
519
+ metadata: Record<string, string>;
520
+ private: true;
521
+ }): Promise<{
522
+ size: number;
523
+ }>;
524
+ }
525
+
427
526
  declare enum StorageProvider {
428
527
  LOCAL = "LOCAL",
429
528
  S3 = "S3",
430
529
  WEBDAV = "WEBDAV"
431
530
  }
432
531
 
433
- export { type ChunkUploadState, DANGEROUS_EXTENSIONS, FILE_MANAGER, type FileInfo, FileLifecycleManager, type FileLifecyclePolicy, type FileManager, type FileMetadata, type FileUploadValidation, type LifecycleResult, LocalFileStorage, type PosixFilePermissions, type S3ObjectAclMode, S3Storage, type S3StorageOptions, type S3UploadUrlMode, type StorageModuleAsyncOptions, type StorageModuleOptions, type StorageModuleOptionsFactory, StorageProvider, type StorageProviderType, type UploadOptions, type UploadResult, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, isFileDenied, isSafeOriginalFileName, sanitizeFileName };
532
+ export { BufferedFileManagerStore, type BufferedObjectStore, type ChunkUploadState, DANGEROUS_EXTENSIONS, type DownloadedStorageObject, FILE_MANAGER, type FileInfo, FileLifecycleManager, type FileLifecyclePolicy, type FileManager, type FileMetadata, type FileUploadValidation, type LifecycleResult, LocalFileStorage, type PosixFilePermissions, type S3ObjectAclMode, S3Storage, type S3StorageOptions, type S3UploadUrlMode, type StorageModuleAsyncOptions, type StorageModuleOptions, type StorageModuleOptionsFactory, StorageProvider, type StorageProviderType, StorageTransferError, type StorageTransferErrorCode, type StorageTransferErrorFactory, type StorageUrlHydrationOptions, UnavailableFileManager, type UploadOptions, type UploadResult, type VerifiedObjectTransferOptions, type VerifiedStorageMetadata, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, hydrateStorageUrls, isFileDenied, isSafeOriginalFileName, sanitizeFileName, transferVerifiedObject, verifyStoredObject };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ReadStream } from 'fs';
2
2
  import { OnModuleDestroy, Type, DynamicModule, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
3
+ import { ReadStream as ReadStream$1 } from 'node:fs';
3
4
 
4
5
  interface UploadOptions {
5
6
  contentType?: string;
@@ -424,10 +425,108 @@ declare class ZuckerStorageModule {
424
425
  private static createAsyncProviders;
425
426
  }
426
427
 
428
+ interface StorageUrlHydrationOptions {
429
+ /** The application must establish ownership before allowing a key to be signed. */
430
+ allowedStorageKeys: ReadonlySet<string>;
431
+ getDownloadUrl: (key: string) => Promise<string>;
432
+ storageKeyField: string;
433
+ urlFields: readonly string[];
434
+ }
435
+ /**
436
+ * Clone a JSON-like response graph and resolve only authorized object keys.
437
+ * Dates/class instances pass through. Signatures are deduplicated within this
438
+ * call, never cached between requests. A signing failure rejects the whole call;
439
+ * generated URLs never mutate the source graph that might later be persisted.
440
+ */
441
+ declare function hydrateStorageUrls<T>(value: T, options: StorageUrlHydrationOptions): Promise<T>;
442
+
443
+ /** Disabled storage preserves startup availability while all data operations remain explicit. */
444
+ declare class UnavailableFileManager implements FileManager {
445
+ readonly providerType: StorageProviderType;
446
+ private readonly message;
447
+ constructor(providerType: StorageProviderType, message?: string);
448
+ isConfigured(): boolean;
449
+ private unavailable;
450
+ upload(_buffer: Buffer, _key: string, _options?: UploadOptions): Promise<UploadResult>;
451
+ uploadStream(_stream: ReadStream$1, _key: string, _options?: UploadOptions): Promise<UploadResult>;
452
+ download(_key: string): Promise<{
453
+ stream: ReadStream$1;
454
+ size: number;
455
+ }>;
456
+ delete(_key: string): Promise<void>;
457
+ exists(_key: string): Promise<boolean>;
458
+ getPublicUrl(_key: string): Promise<string>;
459
+ getDownloadUrl(_key: string): Promise<string>;
460
+ list(_prefix: string): Promise<FileInfo[]>;
461
+ }
462
+
463
+ interface DownloadedStorageObject {
464
+ body: Buffer;
465
+ reportedSize: number;
466
+ }
467
+ interface BufferedObjectStore<TMimeType extends string = string> {
468
+ read(key: string): Promise<DownloadedStorageObject>;
469
+ exists(key: string): Promise<boolean>;
470
+ write(key: string, body: Buffer, options: {
471
+ contentType: TMimeType;
472
+ metadata: Record<string, string>;
473
+ private: true;
474
+ }): Promise<{
475
+ size: number;
476
+ }>;
477
+ }
478
+ type StorageTransferErrorCode = 'STORAGE_NOT_CONFIGURED' | 'OBJECT_SIZE_OUT_OF_BOUNDS' | 'SOURCE_SIZE_MISMATCH' | 'TARGET_SIZE_CONFLICT' | 'TARGET_MIME_CONFLICT' | 'TARGET_HASH_CONFLICT' | 'UPLOAD_SIZE_MISMATCH';
479
+ declare class StorageTransferError extends Error {
480
+ readonly code: StorageTransferErrorCode;
481
+ constructor(code: StorageTransferErrorCode);
482
+ }
483
+ type StorageTransferErrorFactory = (code: StorageTransferErrorCode) => Error;
484
+ interface VerifiedStorageMetadata<TMimeType extends string = string> {
485
+ sha256: string;
486
+ size: number;
487
+ mimeType: TMimeType;
488
+ }
489
+ /** Compare every byte via SHA-256, with explicit size and MIME conflict reasons. */
490
+ declare function verifyStoredObject<TMimeType extends string>(object: DownloadedStorageObject, expected: VerifiedStorageMetadata<TMimeType>, detectMimeType: (body: Buffer) => TMimeType, errorFactory?: StorageTransferErrorFactory): void;
491
+ interface VerifiedObjectTransferOptions<TMimeType extends string> {
492
+ source: DownloadedStorageObject;
493
+ target: BufferedObjectStore<TMimeType>;
494
+ destinationKey: string;
495
+ mode: 'dry-run' | 'apply';
496
+ allowUpload: boolean;
497
+ detectMimeType: (body: Buffer) => TMimeType;
498
+ errorFactory?: StorageTransferErrorFactory;
499
+ }
500
+ /**
501
+ * Copy a bounded source buffer into a private destination, verify existing data
502
+ * before reuse and round-trip every upload before reporting success. Dry-run and
503
+ * disallowed uploads never write. This does not delete sources or commit metadata.
504
+ */
505
+ declare function transferVerifiedObject<TMimeType extends string>(options: VerifiedObjectTransferOptions<TMimeType>): Promise<VerifiedStorageMetadata<TMimeType> & {
506
+ targetDisposition: 'planned' | 'uploaded' | 'reused';
507
+ }>;
508
+
509
+ /** Bounded in-memory transfer adapter over the existing FileManager contract. */
510
+ declare class BufferedFileManagerStore implements BufferedObjectStore {
511
+ private readonly manager;
512
+ private readonly maxObjectBytes;
513
+ private readonly errorFactory;
514
+ constructor(manager: FileManager, maxObjectBytes: number, errorFactory?: StorageTransferErrorFactory);
515
+ read(key: string): Promise<DownloadedStorageObject>;
516
+ exists(key: string): Promise<boolean>;
517
+ write(key: string, body: Buffer, options: {
518
+ contentType: string;
519
+ metadata: Record<string, string>;
520
+ private: true;
521
+ }): Promise<{
522
+ size: number;
523
+ }>;
524
+ }
525
+
427
526
  declare enum StorageProvider {
428
527
  LOCAL = "LOCAL",
429
528
  S3 = "S3",
430
529
  WEBDAV = "WEBDAV"
431
530
  }
432
531
 
433
- export { type ChunkUploadState, DANGEROUS_EXTENSIONS, FILE_MANAGER, type FileInfo, FileLifecycleManager, type FileLifecyclePolicy, type FileManager, type FileMetadata, type FileUploadValidation, type LifecycleResult, LocalFileStorage, type PosixFilePermissions, type S3ObjectAclMode, S3Storage, type S3StorageOptions, type S3UploadUrlMode, type StorageModuleAsyncOptions, type StorageModuleOptions, type StorageModuleOptionsFactory, StorageProvider, type StorageProviderType, type UploadOptions, type UploadResult, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, isFileDenied, isSafeOriginalFileName, sanitizeFileName };
532
+ export { BufferedFileManagerStore, type BufferedObjectStore, type ChunkUploadState, DANGEROUS_EXTENSIONS, type DownloadedStorageObject, FILE_MANAGER, type FileInfo, FileLifecycleManager, type FileLifecyclePolicy, type FileManager, type FileMetadata, type FileUploadValidation, type LifecycleResult, LocalFileStorage, type PosixFilePermissions, type S3ObjectAclMode, S3Storage, type S3StorageOptions, type S3UploadUrlMode, type StorageModuleAsyncOptions, type StorageModuleOptions, type StorageModuleOptionsFactory, StorageProvider, type StorageProviderType, StorageTransferError, type StorageTransferErrorCode, type StorageTransferErrorFactory, type StorageUrlHydrationOptions, UnavailableFileManager, type UploadOptions, type UploadResult, type VerifiedObjectTransferOptions, type VerifiedStorageMetadata, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, hydrateStorageUrls, isFileDenied, isSafeOriginalFileName, sanitizeFileName, transferVerifiedObject, verifyStoredObject };
package/dist/index.js CHANGED
@@ -31,18 +31,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
+ BufferedFileManagerStore: () => BufferedFileManagerStore,
34
35
  DANGEROUS_EXTENSIONS: () => DANGEROUS_EXTENSIONS,
35
36
  FILE_MANAGER: () => FILE_MANAGER,
36
37
  FileLifecycleManager: () => FileLifecycleManager,
37
38
  LocalFileStorage: () => LocalFileStorage,
38
39
  S3Storage: () => S3Storage,
39
40
  StorageProvider: () => StorageProvider,
41
+ StorageTransferError: () => StorageTransferError,
42
+ UnavailableFileManager: () => UnavailableFileManager,
40
43
  WebDavStorage: () => WebDavStorage,
41
44
  ZuckerStorageModule: () => ZuckerStorageModule,
42
45
  generateDateBasedKey: () => generateDateBasedKey,
46
+ hydrateStorageUrls: () => hydrateStorageUrls,
43
47
  isFileDenied: () => isFileDenied,
44
48
  isSafeOriginalFileName: () => isSafeOriginalFileName,
45
- sanitizeFileName: () => sanitizeFileName
49
+ sanitizeFileName: () => sanitizeFileName,
50
+ transferVerifiedObject: () => transferVerifiedObject,
51
+ verifyStoredObject: () => verifyStoredObject
46
52
  });
47
53
  module.exports = __toCommonJS(index_exports);
48
54
 
@@ -1057,6 +1063,198 @@ ZuckerStorageModule = _ts_decorate5([
1057
1063
  (0, import_common5.Module)({})
1058
1064
  ], ZuckerStorageModule);
1059
1065
 
1066
+ // src/storage-url-hydrator.ts
1067
+ function isPlainRecord(value) {
1068
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1069
+ const prototype = Object.getPrototypeOf(value);
1070
+ return prototype === Object.prototype || prototype === null;
1071
+ }
1072
+ __name(isPlainRecord, "isPlainRecord");
1073
+ async function hydrateStorageUrls(value, options) {
1074
+ const signedUrls = /* @__PURE__ */ new Map();
1075
+ const hydrate = /* @__PURE__ */ __name(async (current) => {
1076
+ if (Array.isArray(current)) return Promise.all(current.map(hydrate));
1077
+ if (!isPlainRecord(current)) return current;
1078
+ const entries = await Promise.all(Object.entries(current).map(async ([key2, nested]) => [
1079
+ key2,
1080
+ await hydrate(nested)
1081
+ ]));
1082
+ const result = Object.fromEntries(entries);
1083
+ const key = result[options.storageKeyField];
1084
+ if (typeof key === "string" && key.length > 0 && options.allowedStorageKeys.has(key)) {
1085
+ let signed = signedUrls.get(key);
1086
+ if (!signed) {
1087
+ signed = options.getDownloadUrl(key);
1088
+ signedUrls.set(key, signed);
1089
+ }
1090
+ const url = await signed;
1091
+ for (const field of options.urlFields) {
1092
+ Object.defineProperty(result, field, {
1093
+ value: url,
1094
+ writable: true,
1095
+ enumerable: true,
1096
+ configurable: true
1097
+ });
1098
+ }
1099
+ }
1100
+ return result;
1101
+ }, "hydrate");
1102
+ return hydrate(value);
1103
+ }
1104
+ __name(hydrateStorageUrls, "hydrateStorageUrls");
1105
+
1106
+ // src/unavailable-file-manager.ts
1107
+ var UnavailableFileManager = class {
1108
+ static {
1109
+ __name(this, "UnavailableFileManager");
1110
+ }
1111
+ providerType;
1112
+ message;
1113
+ constructor(providerType, message = `Storage provider ${providerType} is not configured`) {
1114
+ this.providerType = providerType;
1115
+ this.message = message;
1116
+ }
1117
+ isConfigured() {
1118
+ return false;
1119
+ }
1120
+ unavailable() {
1121
+ return new Error(this.message);
1122
+ }
1123
+ upload(_buffer, _key, _options) {
1124
+ return Promise.reject(this.unavailable());
1125
+ }
1126
+ uploadStream(_stream, _key, _options) {
1127
+ return Promise.reject(this.unavailable());
1128
+ }
1129
+ download(_key) {
1130
+ return Promise.reject(this.unavailable());
1131
+ }
1132
+ delete(_key) {
1133
+ return Promise.reject(this.unavailable());
1134
+ }
1135
+ exists(_key) {
1136
+ return Promise.resolve(false);
1137
+ }
1138
+ getPublicUrl(_key) {
1139
+ return Promise.reject(this.unavailable());
1140
+ }
1141
+ getDownloadUrl(_key) {
1142
+ return Promise.reject(this.unavailable());
1143
+ }
1144
+ list(_prefix) {
1145
+ return Promise.resolve([]);
1146
+ }
1147
+ };
1148
+
1149
+ // src/verified-object-transfer.ts
1150
+ var import_node_crypto2 = require("crypto");
1151
+ var StorageTransferError = class extends Error {
1152
+ static {
1153
+ __name(this, "StorageTransferError");
1154
+ }
1155
+ code;
1156
+ constructor(code) {
1157
+ super(code), this.code = code;
1158
+ this.name = "StorageTransferError";
1159
+ }
1160
+ };
1161
+ function verifyStoredObject(object, expected, detectMimeType, errorFactory = (code) => new StorageTransferError(code)) {
1162
+ if (object.reportedSize !== object.body.length || object.body.length !== expected.size) throw errorFactory("TARGET_SIZE_CONFLICT");
1163
+ if (detectMimeType(object.body) !== expected.mimeType) throw errorFactory("TARGET_MIME_CONFLICT");
1164
+ if ((0, import_node_crypto2.createHash)("sha256").update(object.body).digest("hex") !== expected.sha256) throw errorFactory("TARGET_HASH_CONFLICT");
1165
+ }
1166
+ __name(verifyStoredObject, "verifyStoredObject");
1167
+ async function transferVerifiedObject(options) {
1168
+ const fail = options.errorFactory ?? ((code) => new StorageTransferError(code));
1169
+ const body = options.source.body;
1170
+ if (options.source.reportedSize !== body.length || body.length === 0) throw fail("SOURCE_SIZE_MISMATCH");
1171
+ const metadata = {
1172
+ sha256: (0, import_node_crypto2.createHash)("sha256").update(body).digest("hex"),
1173
+ size: body.length,
1174
+ mimeType: options.detectMimeType(body)
1175
+ };
1176
+ if (await options.target.exists(options.destinationKey)) {
1177
+ verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
1178
+ return {
1179
+ ...metadata,
1180
+ targetDisposition: options.mode === "dry-run" ? "planned" : "reused"
1181
+ };
1182
+ }
1183
+ if (options.mode === "dry-run" || !options.allowUpload) return {
1184
+ ...metadata,
1185
+ targetDisposition: "planned"
1186
+ };
1187
+ const uploaded = await options.target.write(options.destinationKey, body, {
1188
+ contentType: metadata.mimeType,
1189
+ metadata: {
1190
+ sha256: metadata.sha256,
1191
+ size: String(metadata.size),
1192
+ mimeType: metadata.mimeType
1193
+ },
1194
+ private: true
1195
+ });
1196
+ if (uploaded.size !== metadata.size) throw fail("UPLOAD_SIZE_MISMATCH");
1197
+ verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
1198
+ return {
1199
+ ...metadata,
1200
+ targetDisposition: "uploaded"
1201
+ };
1202
+ }
1203
+ __name(transferVerifiedObject, "transferVerifiedObject");
1204
+
1205
+ // src/buffered-file-manager-store.ts
1206
+ var BufferedFileManagerStore = class {
1207
+ static {
1208
+ __name(this, "BufferedFileManagerStore");
1209
+ }
1210
+ manager;
1211
+ maxObjectBytes;
1212
+ errorFactory;
1213
+ constructor(manager, maxObjectBytes, errorFactory = (code) => new StorageTransferError(code)) {
1214
+ this.manager = manager;
1215
+ this.maxObjectBytes = maxObjectBytes;
1216
+ this.errorFactory = errorFactory;
1217
+ if (!Number.isSafeInteger(maxObjectBytes) || maxObjectBytes < 1) throw new RangeError("maxObjectBytes must be a positive integer");
1218
+ if (!manager.isConfigured()) throw errorFactory("STORAGE_NOT_CONFIGURED");
1219
+ }
1220
+ async read(key) {
1221
+ const downloaded = await this.manager.download(key);
1222
+ if (downloaded.size < 1 || downloaded.size > this.maxObjectBytes) {
1223
+ downloaded.stream.destroy();
1224
+ throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1225
+ }
1226
+ const chunks = [];
1227
+ let size = 0;
1228
+ for await (const chunk of downloaded.stream) {
1229
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1230
+ size += buffer.length;
1231
+ if (size > this.maxObjectBytes) {
1232
+ downloaded.stream.destroy();
1233
+ throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1234
+ }
1235
+ chunks.push(buffer);
1236
+ }
1237
+ return {
1238
+ body: Buffer.concat(chunks),
1239
+ reportedSize: downloaded.size
1240
+ };
1241
+ }
1242
+ exists(key) {
1243
+ return this.manager.exists(key);
1244
+ }
1245
+ async write(key, body, options) {
1246
+ if (body.length > this.maxObjectBytes) throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1247
+ const result = await this.manager.upload(body, key, {
1248
+ contentType: options.contentType,
1249
+ metadata: options.metadata,
1250
+ acl: "private"
1251
+ });
1252
+ return {
1253
+ size: result.size
1254
+ };
1255
+ }
1256
+ };
1257
+
1060
1258
  // src/index.ts
1061
1259
  var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
1062
1260
  StorageProvider2["LOCAL"] = "LOCAL";
@@ -1066,16 +1264,22 @@ var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
1066
1264
  })({});
1067
1265
  // Annotate the CommonJS export names for ESM import in node:
1068
1266
  0 && (module.exports = {
1267
+ BufferedFileManagerStore,
1069
1268
  DANGEROUS_EXTENSIONS,
1070
1269
  FILE_MANAGER,
1071
1270
  FileLifecycleManager,
1072
1271
  LocalFileStorage,
1073
1272
  S3Storage,
1074
1273
  StorageProvider,
1274
+ StorageTransferError,
1275
+ UnavailableFileManager,
1075
1276
  WebDavStorage,
1076
1277
  ZuckerStorageModule,
1077
1278
  generateDateBasedKey,
1279
+ hydrateStorageUrls,
1078
1280
  isFileDenied,
1079
1281
  isSafeOriginalFileName,
1080
- sanitizeFileName
1282
+ sanitizeFileName,
1283
+ transferVerifiedObject,
1284
+ verifyStoredObject
1081
1285
  });
package/dist/index.mjs CHANGED
@@ -1012,6 +1012,198 @@ ZuckerStorageModule = _ts_decorate5([
1012
1012
  Module({})
1013
1013
  ], ZuckerStorageModule);
1014
1014
 
1015
+ // src/storage-url-hydrator.ts
1016
+ function isPlainRecord(value) {
1017
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1018
+ const prototype = Object.getPrototypeOf(value);
1019
+ return prototype === Object.prototype || prototype === null;
1020
+ }
1021
+ __name(isPlainRecord, "isPlainRecord");
1022
+ async function hydrateStorageUrls(value, options) {
1023
+ const signedUrls = /* @__PURE__ */ new Map();
1024
+ const hydrate = /* @__PURE__ */ __name(async (current) => {
1025
+ if (Array.isArray(current)) return Promise.all(current.map(hydrate));
1026
+ if (!isPlainRecord(current)) return current;
1027
+ const entries = await Promise.all(Object.entries(current).map(async ([key2, nested]) => [
1028
+ key2,
1029
+ await hydrate(nested)
1030
+ ]));
1031
+ const result = Object.fromEntries(entries);
1032
+ const key = result[options.storageKeyField];
1033
+ if (typeof key === "string" && key.length > 0 && options.allowedStorageKeys.has(key)) {
1034
+ let signed = signedUrls.get(key);
1035
+ if (!signed) {
1036
+ signed = options.getDownloadUrl(key);
1037
+ signedUrls.set(key, signed);
1038
+ }
1039
+ const url = await signed;
1040
+ for (const field of options.urlFields) {
1041
+ Object.defineProperty(result, field, {
1042
+ value: url,
1043
+ writable: true,
1044
+ enumerable: true,
1045
+ configurable: true
1046
+ });
1047
+ }
1048
+ }
1049
+ return result;
1050
+ }, "hydrate");
1051
+ return hydrate(value);
1052
+ }
1053
+ __name(hydrateStorageUrls, "hydrateStorageUrls");
1054
+
1055
+ // src/unavailable-file-manager.ts
1056
+ var UnavailableFileManager = class {
1057
+ static {
1058
+ __name(this, "UnavailableFileManager");
1059
+ }
1060
+ providerType;
1061
+ message;
1062
+ constructor(providerType, message = `Storage provider ${providerType} is not configured`) {
1063
+ this.providerType = providerType;
1064
+ this.message = message;
1065
+ }
1066
+ isConfigured() {
1067
+ return false;
1068
+ }
1069
+ unavailable() {
1070
+ return new Error(this.message);
1071
+ }
1072
+ upload(_buffer, _key, _options) {
1073
+ return Promise.reject(this.unavailable());
1074
+ }
1075
+ uploadStream(_stream, _key, _options) {
1076
+ return Promise.reject(this.unavailable());
1077
+ }
1078
+ download(_key) {
1079
+ return Promise.reject(this.unavailable());
1080
+ }
1081
+ delete(_key) {
1082
+ return Promise.reject(this.unavailable());
1083
+ }
1084
+ exists(_key) {
1085
+ return Promise.resolve(false);
1086
+ }
1087
+ getPublicUrl(_key) {
1088
+ return Promise.reject(this.unavailable());
1089
+ }
1090
+ getDownloadUrl(_key) {
1091
+ return Promise.reject(this.unavailable());
1092
+ }
1093
+ list(_prefix) {
1094
+ return Promise.resolve([]);
1095
+ }
1096
+ };
1097
+
1098
+ // src/verified-object-transfer.ts
1099
+ import { createHash } from "crypto";
1100
+ var StorageTransferError = class extends Error {
1101
+ static {
1102
+ __name(this, "StorageTransferError");
1103
+ }
1104
+ code;
1105
+ constructor(code) {
1106
+ super(code), this.code = code;
1107
+ this.name = "StorageTransferError";
1108
+ }
1109
+ };
1110
+ function verifyStoredObject(object, expected, detectMimeType, errorFactory = (code) => new StorageTransferError(code)) {
1111
+ if (object.reportedSize !== object.body.length || object.body.length !== expected.size) throw errorFactory("TARGET_SIZE_CONFLICT");
1112
+ if (detectMimeType(object.body) !== expected.mimeType) throw errorFactory("TARGET_MIME_CONFLICT");
1113
+ if (createHash("sha256").update(object.body).digest("hex") !== expected.sha256) throw errorFactory("TARGET_HASH_CONFLICT");
1114
+ }
1115
+ __name(verifyStoredObject, "verifyStoredObject");
1116
+ async function transferVerifiedObject(options) {
1117
+ const fail = options.errorFactory ?? ((code) => new StorageTransferError(code));
1118
+ const body = options.source.body;
1119
+ if (options.source.reportedSize !== body.length || body.length === 0) throw fail("SOURCE_SIZE_MISMATCH");
1120
+ const metadata = {
1121
+ sha256: createHash("sha256").update(body).digest("hex"),
1122
+ size: body.length,
1123
+ mimeType: options.detectMimeType(body)
1124
+ };
1125
+ if (await options.target.exists(options.destinationKey)) {
1126
+ verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
1127
+ return {
1128
+ ...metadata,
1129
+ targetDisposition: options.mode === "dry-run" ? "planned" : "reused"
1130
+ };
1131
+ }
1132
+ if (options.mode === "dry-run" || !options.allowUpload) return {
1133
+ ...metadata,
1134
+ targetDisposition: "planned"
1135
+ };
1136
+ const uploaded = await options.target.write(options.destinationKey, body, {
1137
+ contentType: metadata.mimeType,
1138
+ metadata: {
1139
+ sha256: metadata.sha256,
1140
+ size: String(metadata.size),
1141
+ mimeType: metadata.mimeType
1142
+ },
1143
+ private: true
1144
+ });
1145
+ if (uploaded.size !== metadata.size) throw fail("UPLOAD_SIZE_MISMATCH");
1146
+ verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
1147
+ return {
1148
+ ...metadata,
1149
+ targetDisposition: "uploaded"
1150
+ };
1151
+ }
1152
+ __name(transferVerifiedObject, "transferVerifiedObject");
1153
+
1154
+ // src/buffered-file-manager-store.ts
1155
+ var BufferedFileManagerStore = class {
1156
+ static {
1157
+ __name(this, "BufferedFileManagerStore");
1158
+ }
1159
+ manager;
1160
+ maxObjectBytes;
1161
+ errorFactory;
1162
+ constructor(manager, maxObjectBytes, errorFactory = (code) => new StorageTransferError(code)) {
1163
+ this.manager = manager;
1164
+ this.maxObjectBytes = maxObjectBytes;
1165
+ this.errorFactory = errorFactory;
1166
+ if (!Number.isSafeInteger(maxObjectBytes) || maxObjectBytes < 1) throw new RangeError("maxObjectBytes must be a positive integer");
1167
+ if (!manager.isConfigured()) throw errorFactory("STORAGE_NOT_CONFIGURED");
1168
+ }
1169
+ async read(key) {
1170
+ const downloaded = await this.manager.download(key);
1171
+ if (downloaded.size < 1 || downloaded.size > this.maxObjectBytes) {
1172
+ downloaded.stream.destroy();
1173
+ throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1174
+ }
1175
+ const chunks = [];
1176
+ let size = 0;
1177
+ for await (const chunk of downloaded.stream) {
1178
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1179
+ size += buffer.length;
1180
+ if (size > this.maxObjectBytes) {
1181
+ downloaded.stream.destroy();
1182
+ throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1183
+ }
1184
+ chunks.push(buffer);
1185
+ }
1186
+ return {
1187
+ body: Buffer.concat(chunks),
1188
+ reportedSize: downloaded.size
1189
+ };
1190
+ }
1191
+ exists(key) {
1192
+ return this.manager.exists(key);
1193
+ }
1194
+ async write(key, body, options) {
1195
+ if (body.length > this.maxObjectBytes) throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1196
+ const result = await this.manager.upload(body, key, {
1197
+ contentType: options.contentType,
1198
+ metadata: options.metadata,
1199
+ acl: "private"
1200
+ });
1201
+ return {
1202
+ size: result.size
1203
+ };
1204
+ }
1205
+ };
1206
+
1015
1207
  // src/index.ts
1016
1208
  var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
1017
1209
  StorageProvider2["LOCAL"] = "LOCAL";
@@ -1020,16 +1212,22 @@ var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
1020
1212
  return StorageProvider2;
1021
1213
  })({});
1022
1214
  export {
1215
+ BufferedFileManagerStore,
1023
1216
  DANGEROUS_EXTENSIONS,
1024
1217
  FILE_MANAGER,
1025
1218
  FileLifecycleManager,
1026
1219
  LocalFileStorage,
1027
1220
  S3Storage,
1028
1221
  StorageProvider,
1222
+ StorageTransferError,
1223
+ UnavailableFileManager,
1029
1224
  WebDavStorage,
1030
1225
  ZuckerStorageModule,
1031
1226
  generateDateBasedKey,
1227
+ hydrateStorageUrls,
1032
1228
  isFileDenied,
1033
1229
  isSafeOriginalFileName,
1034
- sanitizeFileName
1230
+ sanitizeFileName,
1231
+ transferVerifiedObject,
1232
+ verifyStoredObject
1035
1233
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zucker-framework/storage",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",