@warlock.js/core 5.15.0 → 5.17.0

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/esm/cli/commands/migrate.command.mjs +5 -1
  3. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  4. package/esm/cli/commands/seed.command.mjs +2 -1
  5. package/esm/cli/commands/seed.command.mjs.map +1 -1
  6. package/esm/cli/commands/typings-generator.command.mjs +16 -23
  7. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  8. package/esm/config/locale-configuration.mjs +7 -1
  9. package/esm/config/locale-configuration.mjs.map +1 -1
  10. package/esm/connectors/http-connector.d.mts.map +1 -1
  11. package/esm/connectors/http-connector.mjs +1 -18
  12. package/esm/connectors/http-connector.mjs.map +1 -1
  13. package/esm/connectors/read-bound-port.mjs +32 -0
  14. package/esm/connectors/read-bound-port.mjs.map +1 -0
  15. package/esm/dev-server/development-server.mjs +5 -1
  16. package/esm/dev-server/development-server.mjs.map +1 -1
  17. package/esm/dev-server/extract-translation-keys.mjs +68 -0
  18. package/esm/dev-server/extract-translation-keys.mjs.map +1 -0
  19. package/esm/dev-server/translation-keys-sources.mjs +19 -0
  20. package/esm/dev-server/translation-keys-sources.mjs.map +1 -0
  21. package/esm/dev-server/type-generator.mjs +64 -11
  22. package/esm/dev-server/type-generator.mjs.map +1 -1
  23. package/esm/generations/features/shared/merge-web-sitemap-config.mjs +118 -0
  24. package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -0
  25. package/esm/generations/features/sitemap.feature.mjs +50 -47
  26. package/esm/generations/features/sitemap.feature.mjs.map +1 -1
  27. package/esm/generations/stubs.mjs +7 -6
  28. package/esm/generations/stubs.mjs.map +1 -1
  29. package/esm/http/csrf-default-guard.d.mts +32 -0
  30. package/esm/http/csrf-default-guard.d.mts.map +1 -0
  31. package/esm/http/csrf-default-guard.mjs +156 -0
  32. package/esm/http/csrf-default-guard.mjs.map +1 -0
  33. package/esm/http/csrf-origin-policy.d.mts +53 -0
  34. package/esm/http/csrf-origin-policy.d.mts.map +1 -0
  35. package/esm/http/csrf-origin-policy.mjs +104 -0
  36. package/esm/http/csrf-origin-policy.mjs.map +1 -0
  37. package/esm/http/error-codes.d.mts +14 -1
  38. package/esm/http/error-codes.d.mts.map +1 -1
  39. package/esm/http/error-codes.mjs +13 -0
  40. package/esm/http/error-codes.mjs.map +1 -1
  41. package/esm/http/index.d.mts +8 -1
  42. package/esm/http/index.mjs +7 -1
  43. package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
  44. package/esm/http/middleware/idempotency.middleware.mjs +1 -1
  45. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  46. package/esm/http/middleware/inject-request-context.mjs +6 -0
  47. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  48. package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
  49. package/esm/http/middleware/maintenance.middleware.mjs +1 -1
  50. package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
  51. package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
  52. package/esm/http/parse-urlencoded-body.mjs +27 -0
  53. package/esm/http/parse-urlencoded-body.mjs.map +1 -0
  54. package/esm/http/plugins.d.mts.map +1 -1
  55. package/esm/http/plugins.mjs +8 -0
  56. package/esm/http/plugins.mjs.map +1 -1
  57. package/esm/http/request.d.mts +2 -2
  58. package/esm/http/request.d.mts.map +1 -1
  59. package/esm/http/request.mjs +7 -3
  60. package/esm/http/request.mjs.map +1 -1
  61. package/esm/http/response.d.mts +9 -2
  62. package/esm/http/response.d.mts.map +1 -1
  63. package/esm/http/response.mjs +18 -4
  64. package/esm/http/response.mjs.map +1 -1
  65. package/esm/http/uploads/detect-image-format.mjs +59 -0
  66. package/esm/http/uploads/detect-image-format.mjs.map +1 -0
  67. package/esm/http/uploads/generate-image-variant.mjs +46 -0
  68. package/esm/http/uploads/generate-image-variant.mjs.map +1 -0
  69. package/esm/http/uploads/generate-image-variants.d.mts +49 -0
  70. package/esm/http/uploads/generate-image-variants.d.mts.map +1 -0
  71. package/esm/http/uploads/generate-image-variants.mjs +141 -0
  72. package/esm/http/uploads/generate-image-variants.mjs.map +1 -0
  73. package/esm/http/uploads/image-variant-types.d.mts +93 -0
  74. package/esm/http/uploads/image-variant-types.d.mts.map +1 -0
  75. package/esm/http/uploads/image-variants-config-error.d.mts +13 -0
  76. package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -0
  77. package/esm/http/uploads/image-variants-config-error.mjs +17 -0
  78. package/esm/http/uploads/image-variants-config-error.mjs.map +1 -0
  79. package/esm/http/uploads/index.d.mts +4 -0
  80. package/esm/http/uploads/index.mjs +5 -0
  81. package/esm/http/uploads/is-path-inside.mjs +26 -0
  82. package/esm/http/uploads/is-path-inside.mjs.map +1 -0
  83. package/esm/http/uploads/load-variant-source.mjs +42 -0
  84. package/esm/http/uploads/load-variant-source.mjs.map +1 -0
  85. package/esm/http/uploads/matches-if-none-match.mjs +13 -0
  86. package/esm/http/uploads/matches-if-none-match.mjs.map +1 -0
  87. package/esm/http/uploads/parse-uploaded-file-query.mjs +51 -0
  88. package/esm/http/uploads/parse-uploaded-file-query.mjs.map +1 -0
  89. package/esm/http/uploads/read-file-head.mjs +20 -0
  90. package/esm/http/uploads/read-file-head.mjs.map +1 -0
  91. package/esm/http/uploads/resolve-image-variants-config.mjs +91 -0
  92. package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -0
  93. package/esm/http/uploads/resolve-inline-image-content-type.mjs +53 -0
  94. package/esm/http/uploads/resolve-inline-image-content-type.mjs.map +1 -0
  95. package/esm/http/uploads/resolve-original-content-type.mjs +31 -0
  96. package/esm/http/uploads/resolve-original-content-type.mjs.map +1 -0
  97. package/esm/http/uploads/resolve-upload-path.mjs +56 -0
  98. package/esm/http/uploads/resolve-upload-path.mjs.map +1 -0
  99. package/esm/http/uploads/resolve-variant-candidate.mjs +39 -0
  100. package/esm/http/uploads/resolve-variant-candidate.mjs.map +1 -0
  101. package/esm/http/uploads/single-flight.mjs +40 -0
  102. package/esm/http/uploads/single-flight.mjs.map +1 -0
  103. package/esm/http/uploads/uploaded-file.controller.d.mts +24 -0
  104. package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -0
  105. package/esm/http/uploads/uploaded-file.controller.mjs +174 -0
  106. package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -0
  107. package/esm/http/uploads/variant-cache-key.mjs +45 -0
  108. package/esm/http/uploads/variant-cache-key.mjs.map +1 -0
  109. package/esm/http/uploads/variant-generations.mjs +16 -0
  110. package/esm/http/uploads/variant-generations.mjs.map +1 -0
  111. package/esm/http/uploads-types.d.mts +102 -1
  112. package/esm/http/uploads-types.d.mts.map +1 -1
  113. package/esm/http/xmlable.d.mts +14 -0
  114. package/esm/http/xmlable.d.mts.map +1 -0
  115. package/esm/index.d.mts +10 -2
  116. package/esm/index.mjs +8 -2
  117. package/esm/router/types.d.mts +23 -0
  118. package/esm/router/types.d.mts.map +1 -1
  119. package/esm/storage/index.d.mts +1 -0
  120. package/esm/storage/index.mjs +1 -0
  121. package/esm/storage/scoped-storage.d.mts +1 -0
  122. package/esm/storage/scoped-storage.d.mts.map +1 -1
  123. package/esm/storage/scoped-storage.mjs +3 -0
  124. package/esm/storage/scoped-storage.mjs.map +1 -1
  125. package/esm/storage/storage.d.mts +2 -0
  126. package/esm/storage/storage.d.mts.map +1 -1
  127. package/esm/storage/storage.mjs +4 -0
  128. package/esm/storage/storage.mjs.map +1 -1
  129. package/esm/storage/utils/storage-not-initialized-error.d.mts +22 -0
  130. package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -0
  131. package/esm/storage/utils/storage-not-initialized-error.mjs +24 -0
  132. package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -0
  133. package/llms-full.txt +47 -7
  134. package/llms.txt +1 -1
  135. package/package.json +22 -21
  136. package/skills/configure-app/SKILL.md +6 -6
  137. package/skills/create-controller/SKILL.md +14 -0
  138. package/skills/health-checks/SKILL.md +8 -0
  139. package/skills/send-response/SKILL.md +2 -0
  140. package/skills/use-localization/SKILL.md +16 -0
  141. package/skills/use-middleware/SKILL.md +1 -1
  142. package/esm/dev-server/translation-type-generator.mjs +0 -28
  143. package/esm/dev-server/translation-type-generator.mjs.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { uploadsPath } from "../utils/paths.mjs";
2
2
  import { storageConfig, storageConfigurations } from "./config.mjs";
3
3
  import { StorageFile } from "./storage-file.mjs";
4
+ import { StorageNotInitializedError } from "./utils/storage-not-initialized-error.mjs";
4
5
  import { safeFetchToBuffer } from "./utils/safe-fetch.mjs";
5
6
  import { ScopedStorage } from "./scoped-storage.mjs";
6
7
  import { storageDriverContext } from "./context/storage-driver-context.mjs";
@@ -95,10 +96,13 @@ var Storage = class extends ScopedStorage {
95
96
  * Will be enhanced to check AsyncLocalStorage context for multi-tenant support.
96
97
  *
97
98
  * @returns The active storage driver
99
+ * @throws {StorageNotInitializedError} When neither a context driver nor
100
+ * an initialized default driver is available
98
101
  */
99
102
  get activeDriver() {
100
103
  const contextDriver = storageDriverContext.getDriver();
101
104
  if (contextDriver) return contextDriver;
105
+ if (!this._driver) throw new StorageNotInitializedError();
102
106
  return this._driver;
103
107
  }
104
108
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"storage.mjs","names":[],"sources":["../../../../../../../core/src/storage/storage.ts"],"sourcesContent":["import events, { type EventSubscription } from \"@mongez/events\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { Readable } from \"stream\";\nimport type { UploadedFile } from \"../http\";\nimport { uploadsPath } from \"../utils/paths\";\nimport { storageConfig, storageConfigurations } from \"./config\";\nimport { storageDriverContext } from \"./context/storage-driver-context\";\nimport { DOSpacesDriver } from \"./drivers/do-spaces-driver\";\nimport { LocalDriver } from \"./drivers/local-driver\";\nimport { R2Driver } from \"./drivers/r2-driver\";\nimport { S3Driver } from \"./drivers/s3-driver\";\nimport { ScopedStorage } from \"./scoped-storage\";\nimport { StorageFile } from \"./storage-file\";\nimport { safeFetchToBuffer } from \"./utils/safe-fetch\";\nimport type {\n CloudStorageDriverContract,\n CloudStorageDriverOptions,\n DeleteManyResult,\n FileVisibility,\n ListOptions,\n LocalStorageDriverOptions,\n PresignedOptions,\n PresignedUploadOptions,\n PutFromUrlOptions,\n PutOptions,\n R2StorageDriverOptions,\n ScopedStorageContract,\n StorageCopyEventPayload,\n StorageDriverConfig,\n StorageDriverContract,\n StorageDriverName,\n StorageEventHandler,\n StorageEventPayload,\n StorageEventType,\n StorageFileInfo,\n StorageManagerContract,\n StoragePutEventPayload,\n TemporaryTokenValidation,\n} from \"./types\";\n\n/**\n * Storage Manager\n *\n * Provides a unified interface for file storage operations across multiple\n * drivers (local, S3, R2, DigitalOcean Spaces). Extends `ScopedStorage` to\n * inherit all base operations while adding driver management and events.\n *\n * All operations return `StorageFile` instances for a consistent, rich DX.\n *\n * @example\n * ```typescript\n * // Basic usage (uses default driver)\n * const file = await storage.put(buffer, \"uploads/image.jpg\");\n *\n * // With options\n * const file = await storage.put(buffer, \"uploads/image.jpg\", {\n * mimeType: \"image/jpeg\",\n * cacheControl: \"max-age=31536000\"\n * });\n *\n * // Using specific driver (also returns StorageFile)\n * const file = await storage.use(\"s3\").put(buffer, \"path/to/file\");\n *\n * // Get raw driver for advanced use\n * const driver = storage.driver(\"s3\");\n * const data = await driver.put(buffer, \"path/to/file\"); // Returns StorageFileData\n *\n * // Stream operations for large files\n * const stream = await storage.getStream(\"large-file.zip\");\n * await storage.putStream(readableStream, \"output/file.zip\");\n *\n * // Batch operations\n * const results = await storage.deleteMany([\"file1.txt\", \"file2.txt\"]);\n *\n * // Event hooks\n * storage.on(\"afterPut\", ({ location, file }) => {\n * console.log(`Uploaded ${location}`);\n * });\n * ```\n */\nexport class Storage extends ScopedStorage implements StorageManagerContract {\n /**\n * Registered drivers (cached instances)\n * @internal\n */\n protected drivers = new Map<string, StorageDriverContract>();\n\n /**\n * Driver configurations\n * @internal\n */\n protected configs = new Map<string, StorageDriverConfig>();\n\n /**\n * Default driver name\n * @internal\n */\n protected defaultDriverName!: StorageDriverName;\n\n /**\n * Whether the storage has been initialized\n * @internal\n */\n private initialized = false;\n\n /**\n * Create a new Storage manager instance\n *\n * Uses lazy initialization - driver is resolved on first access.\n */\n public constructor() {\n // Temp placeholder - will be replaced on first access\n super(null as unknown as StorageDriverContract);\n }\n\n /**\n * Ensure storage is initialized (lazy initialization)\n *\n * Called automatically on first driver access.\n */\n public async init(): Promise<void> {\n if (this.initialized) return;\n\n // Mark as initialized FIRST to prevent infinite recursion\n this.initialized = true;\n\n // Get default driver name from config\n\n const defaultName = storageConfig(\"default\", \"local\");\n\n this.defaultDriverName = defaultName as StorageDriverName;\n this.loadDriversFromConfig();\n\n // Now set the actual driver\n this._driver = this.resolveDriver(this.defaultDriverName);\n }\n\n /**\n * Reset storage defaults\n */\n public reset(): void {\n this.initialized = false;\n this.drivers.clear();\n this.configs.clear();\n this.defaultDriverName = null as unknown as StorageDriverName;\n this._driver = null as unknown as StorageDriverContract;\n }\n\n /**\n * Get the currently active driver (context-aware in future)\n *\n * Currently returns the default driver.\n * Will be enhanced to check AsyncLocalStorage context for multi-tenant support.\n *\n * @returns The active storage driver\n */\n public override get activeDriver(): StorageDriverContract {\n // Check context for tenant-specific driver\n const contextDriver = storageDriverContext.getDriver();\n\n if (contextDriver) return contextDriver;\n\n return this._driver;\n }\n\n // ============================================================\n // Driver Management\n // ============================================================\n\n /**\n * Load drivers from configuration\n * @internal\n */\n protected loadDriversFromConfig(): void {\n this.registerBuiltInLocalDriver();\n\n const drivers = storageConfig<Record<string, StorageDriverConfig>>(\"drivers\", {});\n\n for (const [name, config] of Object.entries(drivers)) {\n this.configs.set(name, config);\n }\n }\n\n /**\n * Register the built-in `local` driver, rooted at `uploadsPath()`.\n *\n * The storage connector starts unconditionally — unlike database, cache and\n * herald, it does not early-return when `config.get(\"storage\")` is absent —\n * on the stated grounds that `init()` falls back to a built-in local driver so\n * file storage works out of the box. That fallback did not exist: `init()`\n * resolved the default driver *name* and then found no config registered\n * under it, so **any app without `src/config/storage.ts` failed to boot**\n * with `Storage driver \"local\" is not configured`. Only scaffolded apps, which\n * always ship that file, hid it.\n *\n * Registered before the configured drivers so an app that defines its own\n * `local` entry overrides this one rather than fighting it.\n */\n protected registerBuiltInLocalDriver(): void {\n this.configs.set(\n \"local\",\n storageConfigurations.local({\n root: uploadsPath(),\n urlPrefix: \"/uploads\",\n }),\n );\n }\n\n /**\n * Get a scoped storage for a specific driver\n *\n * Returns a `ScopedStorage` instance that wraps the specified driver.\n * Operations on the returned instance also return `StorageFile` objects.\n *\n * @param name - Driver name as defined in configuration\n * @returns ScopedStorage instance for the specified driver\n *\n * @example\n * ```typescript\n * // Upload to S3\n * const s3File = await storage.use(\"s3\").put(buffer, \"images/photo.jpg\");\n *\n * // Upload to local\n * const localFile = await storage.use(\"local\").put(buffer, \"temp/file.txt\");\n *\n * // Both return StorageFile with identical API\n * console.log(s3File.url);\n * console.log(localFile.url);\n * ```\n */\n public use(name: StorageDriverName): ScopedStorageContract {\n return new ScopedStorage(this.getDriver(name));\n }\n\n /**\n * Get a raw driver instance\n *\n * Returns the underlying driver directly for advanced use cases.\n * Unlike `use()`, calling methods on the raw driver returns\n * `StorageFileData` instead of `StorageFile`.\n *\n * @param name - Driver name as defined in configuration\n * @returns Raw driver instance implementing StorageDriverContract\n *\n * @example\n * ```typescript\n * const driver = storage.getDriver(\"s3\");\n * const data = await driver.put(buffer, \"path/to/file\");\n * // data is StorageFileData, not StorageFile\n * ```\n */\n public getDriver(name: StorageDriverName): StorageDriverContract {\n return this.resolveDriver(name);\n }\n\n /**\n * Get root directory of current driver\n */\n public root(apepndedPath?: string): string {\n const rootPath = this.activeDriver.options?.root || \"\";\n\n return path.join(rootPath, apepndedPath || \"\");\n }\n\n /**\n * Use a cloud storage driver with extended cloud capabilities\n *\n * @param name - Cloud driver name (s3, r2, spaces)\n * @returns Driver instance implementing CloudStorageDriverContract\n * @throws Error if driver doesn't support cloud operations\n *\n * @example\n * ```typescript\n * const cloudDriver = storage.useCloud(\"s3\");\n * const presignedUrl = await cloudDriver.getPresignedUrl(\"private/doc.pdf\");\n * ```\n */\n public useCloud(name: StorageDriverName): CloudStorageDriverContract {\n const instance = this.getDriver(name);\n\n if (!this.isCloudDriver(instance)) {\n throw new Error(`Driver \"${name}\" does not support cloud operations`);\n }\n\n return instance as CloudStorageDriverContract;\n }\n\n /**\n * Register a new driver configuration at runtime\n *\n * Allows dynamic driver registration for multi-tenancy or\n * runtime configuration scenarios.\n *\n * @param name - Unique driver name\n * @param config - Driver configuration\n * @returns This instance for chaining\n *\n * @example\n * ```typescript\n * storage.register(\"tenant-s3\", {\n * driver: \"s3\",\n * bucket: \"tenant-bucket\",\n * region: \"us-east-1\",\n * accessKeyId: process.env.TENANT_AWS_KEY,\n * secretAccessKey: process.env.TENANT_AWS_SECRET\n * });\n *\n * await storage.use(\"tenant-s3\").put(buffer, \"file.txt\");\n * ```\n */\n public register(name: StorageDriverName, config: StorageDriverConfig): this {\n this.configs.set(name, config);\n this.drivers.delete(name); // Clear cached instance\n return this;\n }\n\n /**\n * Set the default driver name\n *\n * @param name - Driver name to use as default\n * @returns This instance for chaining\n *\n * @example\n * ```typescript\n * storage.setDefault(\"s3\");\n * await storage.put(buffer, \"file.txt\"); // Now uses S3\n * ```\n */\n public setDefault(name: StorageDriverName): this {\n this.defaultDriverName = name;\n this._driver = this.getDriver(name);\n return this;\n }\n\n /**\n * Check if current driver is a cloud driver\n *\n * @returns Promise resolving to true if the current driver supports cloud operations\n */\n public async isCloud(): Promise<boolean> {\n return this.isCloudDriver(this.activeDriver);\n }\n\n /**\n * Check if a driver instance supports cloud operations\n * @internal\n */\n protected isCloudDriver(driver: StorageDriverContract): driver is CloudStorageDriverContract {\n return \"getPresignedUrl\" in driver;\n }\n\n // ============================================================\n // Event System\n // ============================================================\n\n /**\n * Register an event handler\n *\n * Subscribe to storage events for logging, analytics, or side effects.\n *\n * @param event - Event type to listen for\n * @param handler - Handler function\n * @returns Event subscription for unsubscribing\n *\n * @example\n * ```typescript\n * // Log all uploads\n * storage.on(\"afterPut\", ({ location, file }) => {\n * console.log(`Uploaded ${file?.size} bytes to ${location}`);\n * });\n *\n * // Track deletions\n * storage.on(\"afterDelete\", ({ location }) => {\n * analytics.track(\"file_deleted\", { path: location });\n * });\n * ```\n */\n public on<T extends StorageEventPayload = StorageEventPayload>(\n event: StorageEventType,\n handler: StorageEventHandler<T>,\n ): EventSubscription {\n return events.subscribe(`storage.${event}`, handler);\n }\n\n /**\n * Remove all handlers for an event type\n *\n * @param event - Event type to remove handlers for\n * @returns This instance for chaining\n *\n * @example\n * ```typescript\n * storage.off(\"afterPut\"); // Remove all afterPut handlers\n * ```\n */\n public off(event: StorageEventType): this {\n events.off(`storage.${event}`);\n return this;\n }\n\n /**\n * Emit an event to all registered handlers\n * @internal\n */\n protected async emit<T extends StorageEventPayload>(\n event: StorageEventType,\n payload: T,\n ): Promise<void> {\n // `triggerAll` is synchronous — it never awaits its handlers, so awaiting it\n // would make async listeners fire-and-forget. `triggerAllAsync` takes the\n // same `(event, ...args)` shape and awaits each handler in turn.\n await events.triggerAllAsync(`storage.${event}`, payload);\n }\n\n // ============================================================\n // Overridden Operations with Events\n // ============================================================\n\n /**\n * Store a file in storage\n *\n * Extends base `put()` with event emission for beforePut/afterPut hooks.\n *\n * @param file - File content as Buffer, string, UploadedFile, or Readable stream\n * @param location - Destination path\n * @param options - Storage options (mimeType, cacheControl, etc.)\n * @returns StorageFile instance with cached metadata\n */\n public override async put(\n file: UploadedFile | Buffer | string | Readable,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n const driver = this.activeDriver;\n const buffer = await this.toBuffer(file);\n\n await this.emit<StoragePutEventPayload>(\"beforePut\", {\n driver: driver.name,\n location,\n timestamp: new Date(),\n size: buffer.length,\n });\n\n const result = await driver.put(buffer, location, options);\n\n await this.emit<StoragePutEventPayload>(\"afterPut\", {\n driver: driver.name,\n location,\n timestamp: new Date(),\n file: result,\n });\n\n if (!result.size) {\n result.size = buffer.length;\n }\n\n return StorageFile.fromData(result, driver);\n }\n\n /**\n * Store a file from a readable stream (for large files)\n *\n * Extends base `putStream()` with event emission.\n *\n * @param stream - Readable stream\n * @param location - Destination path\n * @param options - Storage options\n * @returns StorageFile instance with cached metadata\n */\n public override async putStream(\n stream: Readable | string,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n const driver = this.activeDriver;\n\n await this.emit<StoragePutEventPayload>(\"beforePut\", {\n driver: driver.name,\n location,\n timestamp: new Date(),\n });\n\n if (typeof stream === \"string\") {\n stream = fs.createReadStream(stream);\n }\n\n const result = await driver.putStream(stream, location, options);\n\n await this.emit<StoragePutEventPayload>(\"afterPut\", {\n driver: driver.name,\n location,\n timestamp: new Date(),\n file: result,\n });\n\n return StorageFile.fromData(result, driver);\n }\n\n /**\n * Store a file from a URL\n *\n * Downloads content from the URL and stores it at the specified location.\n *\n * The download is SSRF-guarded by default: the URL scheme must be\n * https/http, the host must not resolve to a private / loopback /\n * link-local / cloud-metadata address, the body is capped, and the\n * request times out. Tune or relax via the {@link PutFromUrlOptions}\n * guard fields.\n *\n * @param url - Source URL to download from\n * @param location - Destination path\n * @param options - Storage + outbound-download guard options\n * @returns StorageFile instance with cached metadata\n *\n * @example\n * ```typescript\n * const file = await storage.putFromUrl(\n * \"https://example.com/image.jpg\",\n * \"downloads/image.jpg\"\n * );\n * ```\n */\n public async putFromUrl(\n url: string,\n location: string,\n options?: PutFromUrlOptions,\n ): Promise<StorageFile> {\n const { allowPrivateHosts, maxBytes, timeoutMs, allowedSchemes, ...putOptions } = options ?? {};\n\n const result = await safeFetchToBuffer(url, {\n allowPrivateHosts,\n maxBytes,\n timeoutMs,\n allowedSchemes,\n });\n\n if (!result.ok) {\n throw new Error(`Failed to fetch file from URL: ${result.statusText}`);\n }\n\n if (!result.contentType) {\n throw new Error(`Failed to fetch file from URL: missing content-type header`);\n }\n\n const mimeType = putOptions.mimeType || result.contentType;\n\n return this.put(result.buffer, location, { ...putOptions, mimeType });\n }\n\n /**\n * Store a file from base64 encoded string\n *\n * Decodes base64 content (with optional data URL prefix) and stores it.\n *\n * @param base64 - Base64 encoded file content (or data URL)\n * @param location - Destination path\n * @param options - Storage options\n * @returns StorageFile instance with cached metadata\n *\n * @example\n * ```typescript\n * // From plain base64\n * const file = await storage.putFromBase64(base64String, \"images/photo.jpg\");\n *\n * // From data URL (auto-extracts MIME type)\n * const file = await storage.putFromBase64(\n * \"data:image/png;base64,iVBORw0KGgo...\",\n * \"images/photo.png\"\n * );\n * ```\n */\n public async putFromBase64(\n base64: string,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n let data = base64;\n let mimeType = options?.mimeType;\n\n if (base64.startsWith(\"data:\")) {\n const match = base64.match(/^data:([^;]+);base64,(.+)$/);\n if (match) {\n const matchedMimeType = match[1];\n const matchedData = match[2];\n\n if (matchedMimeType !== undefined && matchedData !== undefined) {\n mimeType = mimeType || matchedMimeType;\n data = matchedData;\n }\n }\n }\n\n const buffer = Buffer.from(data, \"base64\");\n return this.put(buffer, location, { ...options, mimeType });\n }\n\n /**\n * Retrieve file contents as Buffer\n *\n * Uses the current driver (with async resolution).\n *\n * @param location - File path\n * @returns Buffer containing file contents\n */\n public override async get(location: string): Promise<Buffer> {\n return this.activeDriver.get(location);\n }\n\n /**\n * Get JSON content from a file\n *\n * Downloads and parses JSON file content.\n *\n * @param location - File path\n * @returns Parsed JSON content\n *\n * @example\n * ```typescript\n * const config = await storage.getJson(\"config/settings.json\");\n * console.log(config.apiKey);\n * ```\n */\n public async getJson(location: string): Promise<any> {\n const buffer = await this.get(location);\n return JSON.parse(buffer.toString());\n }\n\n /**\n * Retrieve a file as a readable stream (for large files)\n *\n * @param location - File path\n * @returns Readable stream of file contents\n */\n public override async getStream(location: string): Promise<Readable> {\n return this.activeDriver.getStream(location);\n }\n\n /**\n * Delete a file\n *\n * Extends base `delete()` with event emission.\n *\n * @param location - File path or StorageFile\n * @returns true if deleted, false if not found\n */\n public override async delete(location: string | StorageFile): Promise<boolean> {\n const driver = this.activeDriver;\n const path = typeof location === \"string\" ? location : location.path;\n\n await this.emit<StorageEventPayload>(\"beforeDelete\", {\n driver: driver.name,\n location: path,\n timestamp: new Date(),\n });\n\n const result = await driver.delete(path);\n\n await this.emit<StorageEventPayload>(\"afterDelete\", {\n driver: driver.name,\n location: path,\n timestamp: new Date(),\n });\n\n return result;\n }\n\n /**\n * Delete multiple files at once\n *\n * @param locations - Array of file paths\n * @returns Array of delete results with status for each file\n */\n public override async deleteMany(locations: string[]): Promise<DeleteManyResult[]> {\n return this.activeDriver.deleteMany(locations);\n }\n\n /**\n * Check if a file exists\n *\n * @param location - File path\n * @returns true if file exists\n */\n public override async exists(location: string): Promise<boolean> {\n return this.activeDriver.exists(location);\n }\n\n /**\n * Copy a file to a new location\n *\n * Extends base `copy()` with event emission.\n *\n * @param from - Source path or StorageFile\n * @param to - Destination path\n * @returns StorageFile instance at destination\n */\n public override async copy(from: string | StorageFile, to: string): Promise<StorageFile> {\n const driver = this.activeDriver;\n const fromPath = typeof from === \"string\" ? from : from.path;\n\n await this.emit<StorageCopyEventPayload>(\"beforeCopy\", {\n driver: driver.name,\n location: to,\n from: fromPath,\n to,\n timestamp: new Date(),\n });\n\n const result = await driver.copy(fromPath, to);\n\n await this.emit<StorageCopyEventPayload>(\"afterCopy\", {\n driver: driver.name,\n location: to,\n from: fromPath,\n to,\n timestamp: new Date(),\n file: result,\n });\n\n return StorageFile.fromData(result, driver);\n }\n\n /**\n * Move a file to a new location\n *\n * Extends base `move()` with event emission.\n *\n * @param from - Source path or StorageFile\n * @param to - Destination path\n * @returns StorageFile instance at destination\n */\n public override async move(from: string | StorageFile, to: string): Promise<StorageFile> {\n const driver = this.activeDriver;\n const fromPath = typeof from === \"string\" ? from : from.path;\n\n await this.emit<StorageCopyEventPayload>(\"beforeMove\", {\n driver: driver.name,\n location: to,\n from: fromPath,\n to,\n timestamp: new Date(),\n });\n\n const result = await driver.move(fromPath, to);\n\n await this.emit<StorageCopyEventPayload>(\"afterMove\", {\n driver: driver.name,\n location: to,\n from: fromPath,\n to,\n timestamp: new Date(),\n file: result,\n });\n\n return StorageFile.fromData(result, driver);\n }\n\n /**\n * List files in a directory\n *\n * @param directory - Directory path (defaults to root)\n * @param options - List options (recursive, limit, etc.)\n * @returns Array of file information objects\n */\n public override async list(\n directory?: string,\n options?: ListOptions,\n ): Promise<StorageFileInfo[]> {\n return this.activeDriver.list(directory || \"\", options);\n }\n\n // ============================================================\n // Metadata Operations\n // ============================================================\n\n /**\n * Get file metadata without downloading\n *\n * @param location - File path\n * @returns File information object\n */\n public override async metadata(location: string): Promise<StorageFileInfo> {\n return this.activeDriver.metadata(location);\n }\n\n /**\n * Get file size in bytes\n *\n * @param location - File path\n * @returns File size in bytes\n */\n public override async size(location: string): Promise<number> {\n return this.activeDriver.size(location);\n }\n\n // ============================================================\n // Path Operations (Local Driver Only)\n // ============================================================\n\n /**\n * Get the absolute filesystem path for a location\n *\n * Only available for local driver.\n *\n * @param location - File path\n * @throws Error if current driver is not a local driver\n * @returns Absolute filesystem path\n */\n public async path(location: string): Promise<string> {\n const driver = this.activeDriver;\n\n if (!(\"path\" in driver) || typeof driver.path !== \"function\") {\n throw new Error(\"path() is only available for local storage drivers\");\n }\n\n return driver.path(location);\n }\n\n // ============================================================\n // Cloud-Specific Operations\n // ============================================================\n\n /**\n * Get a presigned URL for downloading a file\n *\n * Only available for cloud drivers.\n *\n * @param location - File path\n * @param options - Presigned URL options (expiresIn)\n * @throws Error if current driver is not a cloud driver\n * @returns Presigned download URL\n *\n * @example\n * ```typescript\n * const url = await storage.getPresignedUrl(\"private/document.pdf\", {\n * expiresIn: 3600 // 1 hour\n * });\n * ```\n */\n public async getPresignedUrl(location: string, options?: PresignedOptions): Promise<string> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Presigned URLs are only available for cloud storage drivers\");\n }\n\n return driver.getPresignedUrl(location, options);\n }\n\n /**\n * Get a presigned URL for uploading a file directly to cloud storage\n *\n * Only available for cloud drivers.\n *\n * @param location - Destination path\n * @param options - Upload options (expiresIn, contentType, metadata).\n * There is no size cap: a presigned PUT URL cannot enforce one — verify\n * the uploaded size server-side or use a presigned POST policy instead.\n * @throws Error if current driver is not a cloud driver\n * @returns Presigned upload URL\n *\n * @example\n * ```typescript\n * const uploadUrl = await storage.getPresignedUploadUrl(\"uploads/file.pdf\", {\n * expiresIn: 3600,\n * contentType: \"application/pdf\"\n * });\n *\n * // Client can PUT directly to this URL\n * ```\n */\n public async getPresignedUploadUrl(\n location: string,\n options?: PresignedUploadOptions,\n ): Promise<string> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Presigned upload URLs are only available for cloud storage drivers\");\n }\n\n return driver.getPresignedUploadUrl(location, options);\n }\n\n /**\n * Get the bucket name for cloud storage\n *\n * Only available for cloud drivers.\n *\n * @throws Error if current driver is not a cloud driver\n * @returns Bucket name\n */\n public async getBucket(): Promise<string> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Bucket information is only available for cloud storage drivers\");\n }\n\n return driver.getBucket();\n }\n\n /**\n * Get the region for cloud storage\n *\n * Only available for cloud drivers.\n *\n * @throws Error if current driver is not a cloud driver\n * @returns Region name\n */\n public async getRegion(): Promise<string> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Region information is only available for cloud storage drivers\");\n }\n\n return driver.getRegion();\n }\n\n /**\n * Set storage class for a file (e.g., STANDARD, GLACIER, etc.)\n *\n * Only available for cloud drivers.\n *\n * @param location - File path\n * @param storageClass - Target storage class\n * @throws Error if current driver is not a cloud driver\n */\n public async setStorageClass(location: string, storageClass: string): Promise<void> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Storage class is only available for cloud storage drivers\");\n }\n\n return driver.setStorageClass(location, storageClass);\n }\n\n /**\n * Set file visibility (public or private)\n *\n * Only available for cloud drivers.\n *\n * @param location - File path\n * @param visibility - \"public\" or \"private\"\n * @throws Error if current driver is not a cloud driver\n */\n public async setVisibility(location: string, visibility: FileVisibility): Promise<void> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Visibility is only available for cloud storage drivers\");\n }\n\n return driver.setVisibility(location, visibility);\n }\n\n /**\n * Get file visibility\n *\n * Only available for cloud drivers.\n *\n * @param location - File path\n * @throws Error if current driver is not a cloud driver\n * @returns Current visibility setting\n */\n public async getVisibility(location: string): Promise<FileVisibility> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Visibility is only available for cloud storage drivers\");\n }\n\n return driver.getVisibility(location);\n }\n\n /**\n * Get a temporary signed URL\n *\n * Creates a URL that provides temporary access to the file.\n *\n * @param location - File path\n * @param expiresIn - Seconds until expiration (default: 3600)\n * @returns Signed URL string\n */\n public override async temporaryUrl(location: string, expiresIn?: number): Promise<string> {\n return this.activeDriver.temporaryUrl(location, expiresIn);\n }\n\n /**\n * Validate a temporary URL token\n *\n * For local driver: validates HMAC-signed tokens\n * For cloud drivers: returns invalid (cloud validates via presigned URL)\n *\n * @param token - The token from the temporary URL\n * @returns Validation result with file info and convenience methods\n *\n * @example\n * ```typescript\n * const result = await storage.validateTemporaryToken(token);\n *\n * if (!result.valid) {\n * return response.status(403).send(result.error);\n * }\n *\n * // For local driver - use sendFile for efficiency\n * if (result.absolutePath) {\n * return response.sendFile(result.absolutePath);\n * }\n *\n * // For cloud driver - stream the file\n * const stream = await result.getStream!();\n * stream.pipe(response.raw);\n * ```\n */\n public async validateTemporaryToken(token: string): Promise<TemporaryTokenValidation> {\n // Check if driver supports token validation\n if (\n !(\"validateTemporaryToken\" in this.activeDriver) ||\n typeof this.activeDriver.validateTemporaryToken !== \"function\"\n ) {\n // For cloud drivers, temporary URLs are presigned and validated by the cloud provider\n return {\n valid: false,\n error: \"invalid_token\",\n };\n }\n\n return this.activeDriver.validateTemporaryToken(token);\n }\n\n // ============================================================\n // Configuration Parsing\n // ============================================================\n\n /**\n * Parse config into driver-specific options\n * @internal\n */\n protected parseOptions(\n config: StorageDriverConfig,\n ): LocalStorageDriverOptions | CloudStorageDriverOptions | R2StorageDriverOptions {\n const { driver, ...options } = config;\n\n switch (driver) {\n case \"local\":\n return {\n root: options.root,\n urlPrefix: options.urlPrefix,\n signatureKey: options.signatureKey,\n } satisfies LocalStorageDriverOptions;\n\n case \"s3\":\n this.validateCloudConfig(config, \"s3\");\n return {\n ...options,\n bucket: options.bucket!,\n region: options.region!,\n accessKeyId: options.accessKeyId!,\n secretAccessKey: options.secretAccessKey!,\n endpoint: options.endpoint,\n urlPrefix: options.urlPrefix,\n } satisfies CloudStorageDriverOptions;\n\n case \"r2\":\n this.validateCloudConfig(config, \"r2\");\n if (!options.accountId) {\n throw new Error('R2 driver requires \"accountId\" configuration');\n }\n\n return {\n ...options,\n region: options.region || \"auto\",\n bucket: options.bucket!,\n accessKeyId: options.accessKeyId!,\n secretAccessKey: options.secretAccessKey!,\n endpoint: options.endpoint,\n urlPrefix: options.urlPrefix,\n accountId: options.accountId,\n publicDomain: options.publicDomain,\n } satisfies R2StorageDriverOptions;\n\n case \"spaces\":\n this.validateCloudConfig(config, \"spaces\");\n return {\n ...options,\n bucket: options.bucket!,\n region: options.region!,\n accessKeyId: options.accessKeyId!,\n secretAccessKey: options.secretAccessKey!,\n endpoint: options.endpoint,\n urlPrefix: options.urlPrefix,\n } satisfies CloudStorageDriverOptions;\n\n default:\n throw new Error(`Unknown driver type: ${driver}`);\n }\n }\n\n /**\n * Validate cloud driver configuration has required fields\n * @internal\n */\n protected validateCloudConfig(config: StorageDriverConfig, driverName: string): void {\n const required = [\"bucket\", \"accessKeyId\", \"secretAccessKey\"];\n\n if (driverName !== \"r2\") {\n required.push(\"region\");\n }\n\n for (const field of required) {\n if (!config[field as keyof StorageDriverConfig]) {\n throw new Error(`${driverName.toUpperCase()} driver requires \"${field}\" configuration`);\n }\n }\n }\n\n /**\n * Get or create driver instance from cache\n * @internal\n */\n protected resolveDriver(name: string): StorageDriverContract {\n // Ensure configs are loaded\n\n if (this.drivers.has(name)) {\n return this.drivers.get(name)!;\n }\n\n const config = this.configs.get(name);\n\n if (!config) {\n throw new Error(`Storage driver \"${name}\" is not configured`);\n }\n\n const options = this.parseOptions(config);\n let driver: StorageDriverContract;\n\n switch (config.driver) {\n case \"local\":\n driver = new LocalDriver(options as LocalStorageDriverOptions);\n break;\n case \"s3\":\n driver = new S3Driver(options as CloudStorageDriverOptions);\n break;\n case \"r2\":\n driver = new R2Driver(options as R2StorageDriverOptions);\n break;\n case \"spaces\":\n driver = new DOSpacesDriver(options as CloudStorageDriverOptions);\n break;\n default:\n throw new Error(`Unknown storage driver type: ${config.driver}`);\n }\n\n this.drivers.set(name, driver);\n return driver;\n }\n\n /**\n * Resolve the default driver name (supports async resolver for multi-tenancy)\n * @internal\n */\n protected async resolveDefaultDriver(): Promise<StorageDriverName> {\n const resolver = storageConfig(\"resolver\");\n\n if (resolver) {\n const resolved = await resolver();\n return resolved || this.defaultDriverName;\n }\n\n return this.defaultDriverName;\n }\n}\n\n/**\n * Singleton storage instance\n *\n * Pre-configured storage manager ready for use throughout the application.\n *\n * @example\n * ```typescript\n * import { storage } from \"@warlock.js/core\";\n *\n * const file = await storage.put(buffer, \"uploads/file.txt\");\n * ```\n */\nexport const storage = new Storage();\n"],"mappings":"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"}
1
+ {"version":3,"file":"storage.mjs","names":[],"sources":["../../../../../../../core/src/storage/storage.ts"],"sourcesContent":["import events, { type EventSubscription } from \"@mongez/events\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { Readable } from \"stream\";\nimport type { UploadedFile } from \"../http\";\nimport { uploadsPath } from \"../utils/paths\";\nimport { storageConfig, storageConfigurations } from \"./config\";\nimport { storageDriverContext } from \"./context/storage-driver-context\";\nimport { DOSpacesDriver } from \"./drivers/do-spaces-driver\";\nimport { LocalDriver } from \"./drivers/local-driver\";\nimport { R2Driver } from \"./drivers/r2-driver\";\nimport { S3Driver } from \"./drivers/s3-driver\";\nimport { ScopedStorage } from \"./scoped-storage\";\nimport { StorageFile } from \"./storage-file\";\nimport { safeFetchToBuffer } from \"./utils/safe-fetch\";\nimport { StorageNotInitializedError } from \"./utils/storage-not-initialized-error\";\nimport type {\n CloudStorageDriverContract,\n CloudStorageDriverOptions,\n DeleteManyResult,\n FileVisibility,\n ListOptions,\n LocalStorageDriverOptions,\n PresignedOptions,\n PresignedUploadOptions,\n PutFromUrlOptions,\n PutOptions,\n R2StorageDriverOptions,\n ScopedStorageContract,\n StorageCopyEventPayload,\n StorageDriverConfig,\n StorageDriverContract,\n StorageDriverName,\n StorageEventHandler,\n StorageEventPayload,\n StorageEventType,\n StorageFileInfo,\n StorageManagerContract,\n StoragePutEventPayload,\n TemporaryTokenValidation,\n} from \"./types\";\n\n/**\n * Storage Manager\n *\n * Provides a unified interface for file storage operations across multiple\n * drivers (local, S3, R2, DigitalOcean Spaces). Extends `ScopedStorage` to\n * inherit all base operations while adding driver management and events.\n *\n * All operations return `StorageFile` instances for a consistent, rich DX.\n *\n * @example\n * ```typescript\n * // Basic usage (uses default driver)\n * const file = await storage.put(buffer, \"uploads/image.jpg\");\n *\n * // With options\n * const file = await storage.put(buffer, \"uploads/image.jpg\", {\n * mimeType: \"image/jpeg\",\n * cacheControl: \"max-age=31536000\"\n * });\n *\n * // Using specific driver (also returns StorageFile)\n * const file = await storage.use(\"s3\").put(buffer, \"path/to/file\");\n *\n * // Get raw driver for advanced use\n * const driver = storage.driver(\"s3\");\n * const data = await driver.put(buffer, \"path/to/file\"); // Returns StorageFileData\n *\n * // Stream operations for large files\n * const stream = await storage.getStream(\"large-file.zip\");\n * await storage.putStream(readableStream, \"output/file.zip\");\n *\n * // Batch operations\n * const results = await storage.deleteMany([\"file1.txt\", \"file2.txt\"]);\n *\n * // Event hooks\n * storage.on(\"afterPut\", ({ location, file }) => {\n * console.log(`Uploaded ${location}`);\n * });\n * ```\n */\nexport class Storage extends ScopedStorage implements StorageManagerContract {\n /**\n * Registered drivers (cached instances)\n * @internal\n */\n protected drivers = new Map<string, StorageDriverContract>();\n\n /**\n * Driver configurations\n * @internal\n */\n protected configs = new Map<string, StorageDriverConfig>();\n\n /**\n * Default driver name\n * @internal\n */\n protected defaultDriverName!: StorageDriverName;\n\n /**\n * Whether the storage has been initialized\n * @internal\n */\n private initialized = false;\n\n /**\n * Create a new Storage manager instance\n *\n * Uses lazy initialization - driver is resolved on first access.\n */\n public constructor() {\n // Temp placeholder - will be replaced on first access\n super(null as unknown as StorageDriverContract);\n }\n\n /**\n * Ensure storage is initialized (lazy initialization)\n *\n * Called automatically on first driver access.\n */\n public async init(): Promise<void> {\n if (this.initialized) return;\n\n // Mark as initialized FIRST to prevent infinite recursion\n this.initialized = true;\n\n // Get default driver name from config\n\n const defaultName = storageConfig(\"default\", \"local\");\n\n this.defaultDriverName = defaultName as StorageDriverName;\n this.loadDriversFromConfig();\n\n // Now set the actual driver\n this._driver = this.resolveDriver(this.defaultDriverName);\n }\n\n /**\n * Reset storage defaults\n */\n public reset(): void {\n this.initialized = false;\n this.drivers.clear();\n this.configs.clear();\n this.defaultDriverName = null as unknown as StorageDriverName;\n this._driver = null as unknown as StorageDriverContract;\n }\n\n /**\n * Get the currently active driver (context-aware in future)\n *\n * Currently returns the default driver.\n * Will be enhanced to check AsyncLocalStorage context for multi-tenant support.\n *\n * @returns The active storage driver\n * @throws {StorageNotInitializedError} When neither a context driver nor\n * an initialized default driver is available\n */\n public override get activeDriver(): StorageDriverContract {\n // Check context for tenant-specific driver\n const contextDriver = storageDriverContext.getDriver();\n\n if (contextDriver) return contextDriver;\n\n if (!this._driver) throw new StorageNotInitializedError();\n\n return this._driver;\n }\n\n // ============================================================\n // Driver Management\n // ============================================================\n\n /**\n * Load drivers from configuration\n * @internal\n */\n protected loadDriversFromConfig(): void {\n this.registerBuiltInLocalDriver();\n\n const drivers = storageConfig<Record<string, StorageDriverConfig>>(\"drivers\", {});\n\n for (const [name, config] of Object.entries(drivers)) {\n this.configs.set(name, config);\n }\n }\n\n /**\n * Register the built-in `local` driver, rooted at `uploadsPath()`.\n *\n * The storage connector starts unconditionally — unlike database, cache and\n * herald, it does not early-return when `config.get(\"storage\")` is absent —\n * on the stated grounds that `init()` falls back to a built-in local driver so\n * file storage works out of the box. That fallback did not exist: `init()`\n * resolved the default driver *name* and then found no config registered\n * under it, so **any app without `src/config/storage.ts` failed to boot**\n * with `Storage driver \"local\" is not configured`. Only scaffolded apps, which\n * always ship that file, hid it.\n *\n * Registered before the configured drivers so an app that defines its own\n * `local` entry overrides this one rather than fighting it.\n */\n protected registerBuiltInLocalDriver(): void {\n this.configs.set(\n \"local\",\n storageConfigurations.local({\n root: uploadsPath(),\n urlPrefix: \"/uploads\",\n }),\n );\n }\n\n /**\n * Get a scoped storage for a specific driver\n *\n * Returns a `ScopedStorage` instance that wraps the specified driver.\n * Operations on the returned instance also return `StorageFile` objects.\n *\n * @param name - Driver name as defined in configuration\n * @returns ScopedStorage instance for the specified driver\n *\n * @example\n * ```typescript\n * // Upload to S3\n * const s3File = await storage.use(\"s3\").put(buffer, \"images/photo.jpg\");\n *\n * // Upload to local\n * const localFile = await storage.use(\"local\").put(buffer, \"temp/file.txt\");\n *\n * // Both return StorageFile with identical API\n * console.log(s3File.url);\n * console.log(localFile.url);\n * ```\n */\n public use(name: StorageDriverName): ScopedStorageContract {\n return new ScopedStorage(this.getDriver(name));\n }\n\n /**\n * Get a raw driver instance\n *\n * Returns the underlying driver directly for advanced use cases.\n * Unlike `use()`, calling methods on the raw driver returns\n * `StorageFileData` instead of `StorageFile`.\n *\n * @param name - Driver name as defined in configuration\n * @returns Raw driver instance implementing StorageDriverContract\n *\n * @example\n * ```typescript\n * const driver = storage.getDriver(\"s3\");\n * const data = await driver.put(buffer, \"path/to/file\");\n * // data is StorageFileData, not StorageFile\n * ```\n */\n public getDriver(name: StorageDriverName): StorageDriverContract {\n return this.resolveDriver(name);\n }\n\n /**\n * Get root directory of current driver\n */\n public root(apepndedPath?: string): string {\n const rootPath = this.activeDriver.options?.root || \"\";\n\n return path.join(rootPath, apepndedPath || \"\");\n }\n\n /**\n * Use a cloud storage driver with extended cloud capabilities\n *\n * @param name - Cloud driver name (s3, r2, spaces)\n * @returns Driver instance implementing CloudStorageDriverContract\n * @throws Error if driver doesn't support cloud operations\n *\n * @example\n * ```typescript\n * const cloudDriver = storage.useCloud(\"s3\");\n * const presignedUrl = await cloudDriver.getPresignedUrl(\"private/doc.pdf\");\n * ```\n */\n public useCloud(name: StorageDriverName): CloudStorageDriverContract {\n const instance = this.getDriver(name);\n\n if (!this.isCloudDriver(instance)) {\n throw new Error(`Driver \"${name}\" does not support cloud operations`);\n }\n\n return instance as CloudStorageDriverContract;\n }\n\n /**\n * Register a new driver configuration at runtime\n *\n * Allows dynamic driver registration for multi-tenancy or\n * runtime configuration scenarios.\n *\n * @param name - Unique driver name\n * @param config - Driver configuration\n * @returns This instance for chaining\n *\n * @example\n * ```typescript\n * storage.register(\"tenant-s3\", {\n * driver: \"s3\",\n * bucket: \"tenant-bucket\",\n * region: \"us-east-1\",\n * accessKeyId: process.env.TENANT_AWS_KEY,\n * secretAccessKey: process.env.TENANT_AWS_SECRET\n * });\n *\n * await storage.use(\"tenant-s3\").put(buffer, \"file.txt\");\n * ```\n */\n public register(name: StorageDriverName, config: StorageDriverConfig): this {\n this.configs.set(name, config);\n this.drivers.delete(name); // Clear cached instance\n return this;\n }\n\n /**\n * Set the default driver name\n *\n * @param name - Driver name to use as default\n * @returns This instance for chaining\n *\n * @example\n * ```typescript\n * storage.setDefault(\"s3\");\n * await storage.put(buffer, \"file.txt\"); // Now uses S3\n * ```\n */\n public setDefault(name: StorageDriverName): this {\n this.defaultDriverName = name;\n this._driver = this.getDriver(name);\n return this;\n }\n\n /**\n * Check if current driver is a cloud driver\n *\n * @returns Promise resolving to true if the current driver supports cloud operations\n */\n public async isCloud(): Promise<boolean> {\n return this.isCloudDriver(this.activeDriver);\n }\n\n /**\n * Check if a driver instance supports cloud operations\n * @internal\n */\n protected isCloudDriver(driver: StorageDriverContract): driver is CloudStorageDriverContract {\n return \"getPresignedUrl\" in driver;\n }\n\n // ============================================================\n // Event System\n // ============================================================\n\n /**\n * Register an event handler\n *\n * Subscribe to storage events for logging, analytics, or side effects.\n *\n * @param event - Event type to listen for\n * @param handler - Handler function\n * @returns Event subscription for unsubscribing\n *\n * @example\n * ```typescript\n * // Log all uploads\n * storage.on(\"afterPut\", ({ location, file }) => {\n * console.log(`Uploaded ${file?.size} bytes to ${location}`);\n * });\n *\n * // Track deletions\n * storage.on(\"afterDelete\", ({ location }) => {\n * analytics.track(\"file_deleted\", { path: location });\n * });\n * ```\n */\n public on<T extends StorageEventPayload = StorageEventPayload>(\n event: StorageEventType,\n handler: StorageEventHandler<T>,\n ): EventSubscription {\n return events.subscribe(`storage.${event}`, handler);\n }\n\n /**\n * Remove all handlers for an event type\n *\n * @param event - Event type to remove handlers for\n * @returns This instance for chaining\n *\n * @example\n * ```typescript\n * storage.off(\"afterPut\"); // Remove all afterPut handlers\n * ```\n */\n public off(event: StorageEventType): this {\n events.off(`storage.${event}`);\n return this;\n }\n\n /**\n * Emit an event to all registered handlers\n * @internal\n */\n protected async emit<T extends StorageEventPayload>(\n event: StorageEventType,\n payload: T,\n ): Promise<void> {\n // `triggerAll` is synchronous — it never awaits its handlers, so awaiting it\n // would make async listeners fire-and-forget. `triggerAllAsync` takes the\n // same `(event, ...args)` shape and awaits each handler in turn.\n await events.triggerAllAsync(`storage.${event}`, payload);\n }\n\n // ============================================================\n // Overridden Operations with Events\n // ============================================================\n\n /**\n * Store a file in storage\n *\n * Extends base `put()` with event emission for beforePut/afterPut hooks.\n *\n * @param file - File content as Buffer, string, UploadedFile, or Readable stream\n * @param location - Destination path\n * @param options - Storage options (mimeType, cacheControl, etc.)\n * @returns StorageFile instance with cached metadata\n */\n public override async put(\n file: UploadedFile | Buffer | string | Readable,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n const driver = this.activeDriver;\n const buffer = await this.toBuffer(file);\n\n await this.emit<StoragePutEventPayload>(\"beforePut\", {\n driver: driver.name,\n location,\n timestamp: new Date(),\n size: buffer.length,\n });\n\n const result = await driver.put(buffer, location, options);\n\n await this.emit<StoragePutEventPayload>(\"afterPut\", {\n driver: driver.name,\n location,\n timestamp: new Date(),\n file: result,\n });\n\n if (!result.size) {\n result.size = buffer.length;\n }\n\n return StorageFile.fromData(result, driver);\n }\n\n /**\n * Store a file from a readable stream (for large files)\n *\n * Extends base `putStream()` with event emission.\n *\n * @param stream - Readable stream\n * @param location - Destination path\n * @param options - Storage options\n * @returns StorageFile instance with cached metadata\n */\n public override async putStream(\n stream: Readable | string,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n const driver = this.activeDriver;\n\n await this.emit<StoragePutEventPayload>(\"beforePut\", {\n driver: driver.name,\n location,\n timestamp: new Date(),\n });\n\n if (typeof stream === \"string\") {\n stream = fs.createReadStream(stream);\n }\n\n const result = await driver.putStream(stream, location, options);\n\n await this.emit<StoragePutEventPayload>(\"afterPut\", {\n driver: driver.name,\n location,\n timestamp: new Date(),\n file: result,\n });\n\n return StorageFile.fromData(result, driver);\n }\n\n /**\n * Store a file from a URL\n *\n * Downloads content from the URL and stores it at the specified location.\n *\n * The download is SSRF-guarded by default: the URL scheme must be\n * https/http, the host must not resolve to a private / loopback /\n * link-local / cloud-metadata address, the body is capped, and the\n * request times out. Tune or relax via the {@link PutFromUrlOptions}\n * guard fields.\n *\n * @param url - Source URL to download from\n * @param location - Destination path\n * @param options - Storage + outbound-download guard options\n * @returns StorageFile instance with cached metadata\n *\n * @example\n * ```typescript\n * const file = await storage.putFromUrl(\n * \"https://example.com/image.jpg\",\n * \"downloads/image.jpg\"\n * );\n * ```\n */\n public async putFromUrl(\n url: string,\n location: string,\n options?: PutFromUrlOptions,\n ): Promise<StorageFile> {\n const { allowPrivateHosts, maxBytes, timeoutMs, allowedSchemes, ...putOptions } = options ?? {};\n\n const result = await safeFetchToBuffer(url, {\n allowPrivateHosts,\n maxBytes,\n timeoutMs,\n allowedSchemes,\n });\n\n if (!result.ok) {\n throw new Error(`Failed to fetch file from URL: ${result.statusText}`);\n }\n\n if (!result.contentType) {\n throw new Error(`Failed to fetch file from URL: missing content-type header`);\n }\n\n const mimeType = putOptions.mimeType || result.contentType;\n\n return this.put(result.buffer, location, { ...putOptions, mimeType });\n }\n\n /**\n * Store a file from base64 encoded string\n *\n * Decodes base64 content (with optional data URL prefix) and stores it.\n *\n * @param base64 - Base64 encoded file content (or data URL)\n * @param location - Destination path\n * @param options - Storage options\n * @returns StorageFile instance with cached metadata\n *\n * @example\n * ```typescript\n * // From plain base64\n * const file = await storage.putFromBase64(base64String, \"images/photo.jpg\");\n *\n * // From data URL (auto-extracts MIME type)\n * const file = await storage.putFromBase64(\n * \"data:image/png;base64,iVBORw0KGgo...\",\n * \"images/photo.png\"\n * );\n * ```\n */\n public async putFromBase64(\n base64: string,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n let data = base64;\n let mimeType = options?.mimeType;\n\n if (base64.startsWith(\"data:\")) {\n const match = base64.match(/^data:([^;]+);base64,(.+)$/);\n if (match) {\n const matchedMimeType = match[1];\n const matchedData = match[2];\n\n if (matchedMimeType !== undefined && matchedData !== undefined) {\n mimeType = mimeType || matchedMimeType;\n data = matchedData;\n }\n }\n }\n\n const buffer = Buffer.from(data, \"base64\");\n return this.put(buffer, location, { ...options, mimeType });\n }\n\n /**\n * Retrieve file contents as Buffer\n *\n * Uses the current driver (with async resolution).\n *\n * @param location - File path\n * @returns Buffer containing file contents\n */\n public override async get(location: string): Promise<Buffer> {\n return this.activeDriver.get(location);\n }\n\n /**\n * Get JSON content from a file\n *\n * Downloads and parses JSON file content.\n *\n * @param location - File path\n * @returns Parsed JSON content\n *\n * @example\n * ```typescript\n * const config = await storage.getJson(\"config/settings.json\");\n * console.log(config.apiKey);\n * ```\n */\n public async getJson(location: string): Promise<any> {\n const buffer = await this.get(location);\n return JSON.parse(buffer.toString());\n }\n\n /**\n * Retrieve a file as a readable stream (for large files)\n *\n * @param location - File path\n * @returns Readable stream of file contents\n */\n public override async getStream(location: string): Promise<Readable> {\n return this.activeDriver.getStream(location);\n }\n\n /**\n * Delete a file\n *\n * Extends base `delete()` with event emission.\n *\n * @param location - File path or StorageFile\n * @returns true if deleted, false if not found\n */\n public override async delete(location: string | StorageFile): Promise<boolean> {\n const driver = this.activeDriver;\n const path = typeof location === \"string\" ? location : location.path;\n\n await this.emit<StorageEventPayload>(\"beforeDelete\", {\n driver: driver.name,\n location: path,\n timestamp: new Date(),\n });\n\n const result = await driver.delete(path);\n\n await this.emit<StorageEventPayload>(\"afterDelete\", {\n driver: driver.name,\n location: path,\n timestamp: new Date(),\n });\n\n return result;\n }\n\n /**\n * Delete multiple files at once\n *\n * @param locations - Array of file paths\n * @returns Array of delete results with status for each file\n */\n public override async deleteMany(locations: string[]): Promise<DeleteManyResult[]> {\n return this.activeDriver.deleteMany(locations);\n }\n\n /**\n * Check if a file exists\n *\n * @param location - File path\n * @returns true if file exists\n */\n public override async exists(location: string): Promise<boolean> {\n return this.activeDriver.exists(location);\n }\n\n /**\n * Copy a file to a new location\n *\n * Extends base `copy()` with event emission.\n *\n * @param from - Source path or StorageFile\n * @param to - Destination path\n * @returns StorageFile instance at destination\n */\n public override async copy(from: string | StorageFile, to: string): Promise<StorageFile> {\n const driver = this.activeDriver;\n const fromPath = typeof from === \"string\" ? from : from.path;\n\n await this.emit<StorageCopyEventPayload>(\"beforeCopy\", {\n driver: driver.name,\n location: to,\n from: fromPath,\n to,\n timestamp: new Date(),\n });\n\n const result = await driver.copy(fromPath, to);\n\n await this.emit<StorageCopyEventPayload>(\"afterCopy\", {\n driver: driver.name,\n location: to,\n from: fromPath,\n to,\n timestamp: new Date(),\n file: result,\n });\n\n return StorageFile.fromData(result, driver);\n }\n\n /**\n * Move a file to a new location\n *\n * Extends base `move()` with event emission.\n *\n * @param from - Source path or StorageFile\n * @param to - Destination path\n * @returns StorageFile instance at destination\n */\n public override async move(from: string | StorageFile, to: string): Promise<StorageFile> {\n const driver = this.activeDriver;\n const fromPath = typeof from === \"string\" ? from : from.path;\n\n await this.emit<StorageCopyEventPayload>(\"beforeMove\", {\n driver: driver.name,\n location: to,\n from: fromPath,\n to,\n timestamp: new Date(),\n });\n\n const result = await driver.move(fromPath, to);\n\n await this.emit<StorageCopyEventPayload>(\"afterMove\", {\n driver: driver.name,\n location: to,\n from: fromPath,\n to,\n timestamp: new Date(),\n file: result,\n });\n\n return StorageFile.fromData(result, driver);\n }\n\n /**\n * List files in a directory\n *\n * @param directory - Directory path (defaults to root)\n * @param options - List options (recursive, limit, etc.)\n * @returns Array of file information objects\n */\n public override async list(\n directory?: string,\n options?: ListOptions,\n ): Promise<StorageFileInfo[]> {\n return this.activeDriver.list(directory || \"\", options);\n }\n\n // ============================================================\n // Metadata Operations\n // ============================================================\n\n /**\n * Get file metadata without downloading\n *\n * @param location - File path\n * @returns File information object\n */\n public override async metadata(location: string): Promise<StorageFileInfo> {\n return this.activeDriver.metadata(location);\n }\n\n /**\n * Get file size in bytes\n *\n * @param location - File path\n * @returns File size in bytes\n */\n public override async size(location: string): Promise<number> {\n return this.activeDriver.size(location);\n }\n\n // ============================================================\n // Path Operations (Local Driver Only)\n // ============================================================\n\n /**\n * Get the absolute filesystem path for a location\n *\n * Only available for local driver.\n *\n * @param location - File path\n * @throws Error if current driver is not a local driver\n * @returns Absolute filesystem path\n */\n public async path(location: string): Promise<string> {\n const driver = this.activeDriver;\n\n if (!(\"path\" in driver) || typeof driver.path !== \"function\") {\n throw new Error(\"path() is only available for local storage drivers\");\n }\n\n return driver.path(location);\n }\n\n // ============================================================\n // Cloud-Specific Operations\n // ============================================================\n\n /**\n * Get a presigned URL for downloading a file\n *\n * Only available for cloud drivers.\n *\n * @param location - File path\n * @param options - Presigned URL options (expiresIn)\n * @throws Error if current driver is not a cloud driver\n * @returns Presigned download URL\n *\n * @example\n * ```typescript\n * const url = await storage.getPresignedUrl(\"private/document.pdf\", {\n * expiresIn: 3600 // 1 hour\n * });\n * ```\n */\n public async getPresignedUrl(location: string, options?: PresignedOptions): Promise<string> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Presigned URLs are only available for cloud storage drivers\");\n }\n\n return driver.getPresignedUrl(location, options);\n }\n\n /**\n * Get a presigned URL for uploading a file directly to cloud storage\n *\n * Only available for cloud drivers.\n *\n * @param location - Destination path\n * @param options - Upload options (expiresIn, contentType, metadata).\n * There is no size cap: a presigned PUT URL cannot enforce one — verify\n * the uploaded size server-side or use a presigned POST policy instead.\n * @throws Error if current driver is not a cloud driver\n * @returns Presigned upload URL\n *\n * @example\n * ```typescript\n * const uploadUrl = await storage.getPresignedUploadUrl(\"uploads/file.pdf\", {\n * expiresIn: 3600,\n * contentType: \"application/pdf\"\n * });\n *\n * // Client can PUT directly to this URL\n * ```\n */\n public async getPresignedUploadUrl(\n location: string,\n options?: PresignedUploadOptions,\n ): Promise<string> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Presigned upload URLs are only available for cloud storage drivers\");\n }\n\n return driver.getPresignedUploadUrl(location, options);\n }\n\n /**\n * Get the bucket name for cloud storage\n *\n * Only available for cloud drivers.\n *\n * @throws Error if current driver is not a cloud driver\n * @returns Bucket name\n */\n public async getBucket(): Promise<string> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Bucket information is only available for cloud storage drivers\");\n }\n\n return driver.getBucket();\n }\n\n /**\n * Get the region for cloud storage\n *\n * Only available for cloud drivers.\n *\n * @throws Error if current driver is not a cloud driver\n * @returns Region name\n */\n public async getRegion(): Promise<string> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Region information is only available for cloud storage drivers\");\n }\n\n return driver.getRegion();\n }\n\n /**\n * Set storage class for a file (e.g., STANDARD, GLACIER, etc.)\n *\n * Only available for cloud drivers.\n *\n * @param location - File path\n * @param storageClass - Target storage class\n * @throws Error if current driver is not a cloud driver\n */\n public async setStorageClass(location: string, storageClass: string): Promise<void> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Storage class is only available for cloud storage drivers\");\n }\n\n return driver.setStorageClass(location, storageClass);\n }\n\n /**\n * Set file visibility (public or private)\n *\n * Only available for cloud drivers.\n *\n * @param location - File path\n * @param visibility - \"public\" or \"private\"\n * @throws Error if current driver is not a cloud driver\n */\n public async setVisibility(location: string, visibility: FileVisibility): Promise<void> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Visibility is only available for cloud storage drivers\");\n }\n\n return driver.setVisibility(location, visibility);\n }\n\n /**\n * Get file visibility\n *\n * Only available for cloud drivers.\n *\n * @param location - File path\n * @throws Error if current driver is not a cloud driver\n * @returns Current visibility setting\n */\n public async getVisibility(location: string): Promise<FileVisibility> {\n const driver = this.activeDriver;\n\n if (!this.isCloudDriver(driver)) {\n throw new Error(\"Visibility is only available for cloud storage drivers\");\n }\n\n return driver.getVisibility(location);\n }\n\n /**\n * Get a temporary signed URL\n *\n * Creates a URL that provides temporary access to the file.\n *\n * @param location - File path\n * @param expiresIn - Seconds until expiration (default: 3600)\n * @returns Signed URL string\n */\n public override async temporaryUrl(location: string, expiresIn?: number): Promise<string> {\n return this.activeDriver.temporaryUrl(location, expiresIn);\n }\n\n /**\n * Validate a temporary URL token\n *\n * For local driver: validates HMAC-signed tokens\n * For cloud drivers: returns invalid (cloud validates via presigned URL)\n *\n * @param token - The token from the temporary URL\n * @returns Validation result with file info and convenience methods\n *\n * @example\n * ```typescript\n * const result = await storage.validateTemporaryToken(token);\n *\n * if (!result.valid) {\n * return response.status(403).send(result.error);\n * }\n *\n * // For local driver - use sendFile for efficiency\n * if (result.absolutePath) {\n * return response.sendFile(result.absolutePath);\n * }\n *\n * // For cloud driver - stream the file\n * const stream = await result.getStream!();\n * stream.pipe(response.raw);\n * ```\n */\n public async validateTemporaryToken(token: string): Promise<TemporaryTokenValidation> {\n // Check if driver supports token validation\n if (\n !(\"validateTemporaryToken\" in this.activeDriver) ||\n typeof this.activeDriver.validateTemporaryToken !== \"function\"\n ) {\n // For cloud drivers, temporary URLs are presigned and validated by the cloud provider\n return {\n valid: false,\n error: \"invalid_token\",\n };\n }\n\n return this.activeDriver.validateTemporaryToken(token);\n }\n\n // ============================================================\n // Configuration Parsing\n // ============================================================\n\n /**\n * Parse config into driver-specific options\n * @internal\n */\n protected parseOptions(\n config: StorageDriverConfig,\n ): LocalStorageDriverOptions | CloudStorageDriverOptions | R2StorageDriverOptions {\n const { driver, ...options } = config;\n\n switch (driver) {\n case \"local\":\n return {\n root: options.root,\n urlPrefix: options.urlPrefix,\n signatureKey: options.signatureKey,\n } satisfies LocalStorageDriverOptions;\n\n case \"s3\":\n this.validateCloudConfig(config, \"s3\");\n return {\n ...options,\n bucket: options.bucket!,\n region: options.region!,\n accessKeyId: options.accessKeyId!,\n secretAccessKey: options.secretAccessKey!,\n endpoint: options.endpoint,\n urlPrefix: options.urlPrefix,\n } satisfies CloudStorageDriverOptions;\n\n case \"r2\":\n this.validateCloudConfig(config, \"r2\");\n if (!options.accountId) {\n throw new Error('R2 driver requires \"accountId\" configuration');\n }\n\n return {\n ...options,\n region: options.region || \"auto\",\n bucket: options.bucket!,\n accessKeyId: options.accessKeyId!,\n secretAccessKey: options.secretAccessKey!,\n endpoint: options.endpoint,\n urlPrefix: options.urlPrefix,\n accountId: options.accountId,\n publicDomain: options.publicDomain,\n } satisfies R2StorageDriverOptions;\n\n case \"spaces\":\n this.validateCloudConfig(config, \"spaces\");\n return {\n ...options,\n bucket: options.bucket!,\n region: options.region!,\n accessKeyId: options.accessKeyId!,\n secretAccessKey: options.secretAccessKey!,\n endpoint: options.endpoint,\n urlPrefix: options.urlPrefix,\n } satisfies CloudStorageDriverOptions;\n\n default:\n throw new Error(`Unknown driver type: ${driver}`);\n }\n }\n\n /**\n * Validate cloud driver configuration has required fields\n * @internal\n */\n protected validateCloudConfig(config: StorageDriverConfig, driverName: string): void {\n const required = [\"bucket\", \"accessKeyId\", \"secretAccessKey\"];\n\n if (driverName !== \"r2\") {\n required.push(\"region\");\n }\n\n for (const field of required) {\n if (!config[field as keyof StorageDriverConfig]) {\n throw new Error(`${driverName.toUpperCase()} driver requires \"${field}\" configuration`);\n }\n }\n }\n\n /**\n * Get or create driver instance from cache\n * @internal\n */\n protected resolveDriver(name: string): StorageDriverContract {\n // Ensure configs are loaded\n\n if (this.drivers.has(name)) {\n return this.drivers.get(name)!;\n }\n\n const config = this.configs.get(name);\n\n if (!config) {\n throw new Error(`Storage driver \"${name}\" is not configured`);\n }\n\n const options = this.parseOptions(config);\n let driver: StorageDriverContract;\n\n switch (config.driver) {\n case \"local\":\n driver = new LocalDriver(options as LocalStorageDriverOptions);\n break;\n case \"s3\":\n driver = new S3Driver(options as CloudStorageDriverOptions);\n break;\n case \"r2\":\n driver = new R2Driver(options as R2StorageDriverOptions);\n break;\n case \"spaces\":\n driver = new DOSpacesDriver(options as CloudStorageDriverOptions);\n break;\n default:\n throw new Error(`Unknown storage driver type: ${config.driver}`);\n }\n\n this.drivers.set(name, driver);\n return driver;\n }\n\n /**\n * Resolve the default driver name (supports async resolver for multi-tenancy)\n * @internal\n */\n protected async resolveDefaultDriver(): Promise<StorageDriverName> {\n const resolver = storageConfig(\"resolver\");\n\n if (resolver) {\n const resolved = await resolver();\n return resolved || this.defaultDriverName;\n }\n\n return this.defaultDriverName;\n }\n}\n\n/**\n * Singleton storage instance\n *\n * Pre-configured storage manager ready for use throughout the application.\n *\n * @example\n * ```typescript\n * import { storage } from \"@warlock.js/core\";\n *\n * const file = await storage.put(buffer, \"uploads/file.txt\");\n * ```\n */\nexport const storage = new Storage();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,IAAa,UAAb,cAA6B,cAAgD;;;;;;CA8B3E,AAAO,cAAc;EAEnB,MAAM,IAAwC;iCA3B5B,IAAI,IAAmC;iCAMvC,IAAI,IAAiC;qBAYnC;CAUtB;;;;;;CAOA,MAAa,OAAsB;EACjC,IAAI,KAAK,aAAa;EAGtB,KAAK,cAAc;EAInB,MAAM,cAAc,cAAc,WAAW,OAAO;EAEpD,KAAK,oBAAoB;EACzB,KAAK,sBAAsB;EAG3B,KAAK,UAAU,KAAK,cAAc,KAAK,iBAAiB;CAC1D;;;;CAKA,AAAO,QAAc;EACnB,KAAK,cAAc;EACnB,KAAK,QAAQ,MAAM;EACnB,KAAK,QAAQ,MAAM;EACnB,KAAK,oBAAoB;EACzB,KAAK,UAAU;CACjB;;;;;;;;;;;CAYA,IAAoB,eAAsC;EAExD,MAAM,gBAAgB,qBAAqB,UAAU;EAErD,IAAI,eAAe,OAAO;EAE1B,IAAI,CAAC,KAAK,SAAS,MAAM,IAAI,2BAA2B;EAExD,OAAO,KAAK;CACd;;;;;CAUA,AAAU,wBAA8B;EACtC,KAAK,2BAA2B;EAEhC,MAAM,UAAU,cAAmD,WAAW,CAAC,CAAC;EAEhF,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,OAAO,GACjD,KAAK,QAAQ,IAAI,MAAM,MAAM;CAEjC;;;;;;;;;;;;;;;;CAiBA,AAAU,6BAAmC;EAC3C,KAAK,QAAQ,IACX,SACA,sBAAsB,MAAM;GAC1B,MAAM,YAAY;GAClB,WAAW;EACb,CAAC,CACH;CACF;;;;;;;;;;;;;;;;;;;;;;;CAwBA,AAAO,IAAI,MAAgD;EACzD,OAAO,IAAI,cAAc,KAAK,UAAU,IAAI,CAAC;CAC/C;;;;;;;;;;;;;;;;;;CAmBA,AAAO,UAAU,MAAgD;EAC/D,OAAO,KAAK,cAAc,IAAI;CAChC;;;;CAKA,AAAO,KAAK,cAA+B;EACzC,MAAM,WAAW,KAAK,aAAa,SAAS,QAAQ;EAEpD,OAAO,KAAK,KAAK,UAAU,gBAAgB,EAAE;CAC/C;;;;;;;;;;;;;;CAeA,AAAO,SAAS,MAAqD;EACnE,MAAM,WAAW,KAAK,UAAU,IAAI;EAEpC,IAAI,CAAC,KAAK,cAAc,QAAQ,GAC9B,MAAM,IAAI,MAAM,WAAW,KAAK,oCAAoC;EAGtE,OAAO;CACT;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,AAAO,SAAS,MAAyB,QAAmC;EAC1E,KAAK,QAAQ,IAAI,MAAM,MAAM;EAC7B,KAAK,QAAQ,OAAO,IAAI;EACxB,OAAO;CACT;;;;;;;;;;;;;CAcA,AAAO,WAAW,MAA+B;EAC/C,KAAK,oBAAoB;EACzB,KAAK,UAAU,KAAK,UAAU,IAAI;EAClC,OAAO;CACT;;;;;;CAOA,MAAa,UAA4B;EACvC,OAAO,KAAK,cAAc,KAAK,YAAY;CAC7C;;;;;CAMA,AAAU,cAAc,QAAqE;EAC3F,OAAO,qBAAqB;CAC9B;;;;;;;;;;;;;;;;;;;;;;;CA4BA,AAAO,GACL,OACA,SACmB;EACnB,OAAO,OAAO,UAAU,WAAW,SAAS,OAAO;CACrD;;;;;;;;;;;;CAaA,AAAO,IAAI,OAA+B;EACxC,OAAO,IAAI,WAAW,OAAO;EAC7B,OAAO;CACT;;;;;CAMA,MAAgB,KACd,OACA,SACe;EAIf,MAAM,OAAO,gBAAgB,WAAW,SAAS,OAAO;CAC1D;;;;;;;;;;;CAgBA,MAAsB,IACpB,MACA,UACA,SACsB;EACtB,MAAM,SAAS,KAAK;EACpB,MAAM,SAAS,MAAM,KAAK,SAAS,IAAI;EAEvC,MAAM,KAAK,KAA6B,aAAa;GACnD,QAAQ,OAAO;GACf;GACA,2BAAW,IAAI,KAAK;GACpB,MAAM,OAAO;EACf,CAAC;EAED,MAAM,SAAS,MAAM,OAAO,IAAI,QAAQ,UAAU,OAAO;EAEzD,MAAM,KAAK,KAA6B,YAAY;GAClD,QAAQ,OAAO;GACf;GACA,2BAAW,IAAI,KAAK;GACpB,MAAM;EACR,CAAC;EAED,IAAI,CAAC,OAAO,MACV,OAAO,OAAO,OAAO;EAGvB,OAAO,YAAY,SAAS,QAAQ,MAAM;CAC5C;;;;;;;;;;;CAYA,MAAsB,UACpB,QACA,UACA,SACsB;EACtB,MAAM,SAAS,KAAK;EAEpB,MAAM,KAAK,KAA6B,aAAa;GACnD,QAAQ,OAAO;GACf;GACA,2BAAW,IAAI,KAAK;EACtB,CAAC;EAED,IAAI,OAAO,WAAW,UACpB,SAAS,GAAG,iBAAiB,MAAM;EAGrC,MAAM,SAAS,MAAM,OAAO,UAAU,QAAQ,UAAU,OAAO;EAE/D,MAAM,KAAK,KAA6B,YAAY;GAClD,QAAQ,OAAO;GACf;GACA,2BAAW,IAAI,KAAK;GACpB,MAAM;EACR,CAAC;EAED,OAAO,YAAY,SAAS,QAAQ,MAAM;CAC5C;;;;;;;;;;;;;;;;;;;;;;;;;CA0BA,MAAa,WACX,KACA,UACA,SACsB;EACtB,MAAM,EAAE,mBAAmB,UAAU,WAAW,gBAAgB,GAAG,eAAe,WAAW,CAAC;EAE9F,MAAM,SAAS,MAAM,kBAAkB,KAAK;GAC1C;GACA;GACA;GACA;EACF,CAAC;EAED,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,MAAM,kCAAkC,OAAO,YAAY;EAGvE,IAAI,CAAC,OAAO,aACV,MAAM,IAAI,MAAM,4DAA4D;EAG9E,MAAM,WAAW,WAAW,YAAY,OAAO;EAE/C,OAAO,KAAK,IAAI,OAAO,QAAQ,UAAU;GAAE,GAAG;GAAY;EAAS,CAAC;CACtE;;;;;;;;;;;;;;;;;;;;;;;CAwBA,MAAa,cACX,QACA,UACA,SACsB;EACtB,IAAI,OAAO;EACX,IAAI,WAAW,SAAS;EAExB,IAAI,OAAO,WAAW,OAAO,GAAG;GAC9B,MAAM,QAAQ,OAAO,MAAM,4BAA4B;GACvD,IAAI,OAAO;IACT,MAAM,kBAAkB,MAAM;IAC9B,MAAM,cAAc,MAAM;IAE1B,IAAI,oBAAoB,UAAa,gBAAgB,QAAW;KAC9D,WAAW,YAAY;KACvB,OAAO;IACT;GACF;EACF;EAEA,MAAM,SAAS,OAAO,KAAK,MAAM,QAAQ;EACzC,OAAO,KAAK,IAAI,QAAQ,UAAU;GAAE,GAAG;GAAS;EAAS,CAAC;CAC5D;;;;;;;;;CAUA,MAAsB,IAAI,UAAmC;EAC3D,OAAO,KAAK,aAAa,IAAI,QAAQ;CACvC;;;;;;;;;;;;;;;CAgBA,MAAa,QAAQ,UAAgC;EACnD,MAAM,SAAS,MAAM,KAAK,IAAI,QAAQ;EACtC,OAAO,KAAK,MAAM,OAAO,SAAS,CAAC;CACrC;;;;;;;CAQA,MAAsB,UAAU,UAAqC;EACnE,OAAO,KAAK,aAAa,UAAU,QAAQ;CAC7C;;;;;;;;;CAUA,MAAsB,OAAO,UAAkD;EAC7E,MAAM,SAAS,KAAK;EACpB,MAAM,OAAO,OAAO,aAAa,WAAW,WAAW,SAAS;EAEhE,MAAM,KAAK,KAA0B,gBAAgB;GACnD,QAAQ,OAAO;GACf,UAAU;GACV,2BAAW,IAAI,KAAK;EACtB,CAAC;EAED,MAAM,SAAS,MAAM,OAAO,OAAO,IAAI;EAEvC,MAAM,KAAK,KAA0B,eAAe;GAClD,QAAQ,OAAO;GACf,UAAU;GACV,2BAAW,IAAI,KAAK;EACtB,CAAC;EAED,OAAO;CACT;;;;;;;CAQA,MAAsB,WAAW,WAAkD;EACjF,OAAO,KAAK,aAAa,WAAW,SAAS;CAC/C;;;;;;;CAQA,MAAsB,OAAO,UAAoC;EAC/D,OAAO,KAAK,aAAa,OAAO,QAAQ;CAC1C;;;;;;;;;;CAWA,MAAsB,KAAK,MAA4B,IAAkC;EACvF,MAAM,SAAS,KAAK;EACpB,MAAM,WAAW,OAAO,SAAS,WAAW,OAAO,KAAK;EAExD,MAAM,KAAK,KAA8B,cAAc;GACrD,QAAQ,OAAO;GACf,UAAU;GACV,MAAM;GACN;GACA,2BAAW,IAAI,KAAK;EACtB,CAAC;EAED,MAAM,SAAS,MAAM,OAAO,KAAK,UAAU,EAAE;EAE7C,MAAM,KAAK,KAA8B,aAAa;GACpD,QAAQ,OAAO;GACf,UAAU;GACV,MAAM;GACN;GACA,2BAAW,IAAI,KAAK;GACpB,MAAM;EACR,CAAC;EAED,OAAO,YAAY,SAAS,QAAQ,MAAM;CAC5C;;;;;;;;;;CAWA,MAAsB,KAAK,MAA4B,IAAkC;EACvF,MAAM,SAAS,KAAK;EACpB,MAAM,WAAW,OAAO,SAAS,WAAW,OAAO,KAAK;EAExD,MAAM,KAAK,KAA8B,cAAc;GACrD,QAAQ,OAAO;GACf,UAAU;GACV,MAAM;GACN;GACA,2BAAW,IAAI,KAAK;EACtB,CAAC;EAED,MAAM,SAAS,MAAM,OAAO,KAAK,UAAU,EAAE;EAE7C,MAAM,KAAK,KAA8B,aAAa;GACpD,QAAQ,OAAO;GACf,UAAU;GACV,MAAM;GACN;GACA,2BAAW,IAAI,KAAK;GACpB,MAAM;EACR,CAAC;EAED,OAAO,YAAY,SAAS,QAAQ,MAAM;CAC5C;;;;;;;;CASA,MAAsB,KACpB,WACA,SAC4B;EAC5B,OAAO,KAAK,aAAa,KAAK,aAAa,IAAI,OAAO;CACxD;;;;;;;CAYA,MAAsB,SAAS,UAA4C;EACzE,OAAO,KAAK,aAAa,SAAS,QAAQ;CAC5C;;;;;;;CAQA,MAAsB,KAAK,UAAmC;EAC5D,OAAO,KAAK,aAAa,KAAK,QAAQ;CACxC;;;;;;;;;;CAeA,MAAa,KAAK,UAAmC;EACnD,MAAM,SAAS,KAAK;EAEpB,IAAI,EAAE,UAAU,WAAW,OAAO,OAAO,SAAS,YAChD,MAAM,IAAI,MAAM,oDAAoD;EAGtE,OAAO,OAAO,KAAK,QAAQ;CAC7B;;;;;;;;;;;;;;;;;;CAuBA,MAAa,gBAAgB,UAAkB,SAA6C;EAC1F,MAAM,SAAS,KAAK;EAEpB,IAAI,CAAC,KAAK,cAAc,MAAM,GAC5B,MAAM,IAAI,MAAM,6DAA6D;EAG/E,OAAO,OAAO,gBAAgB,UAAU,OAAO;CACjD;;;;;;;;;;;;;;;;;;;;;;;CAwBA,MAAa,sBACX,UACA,SACiB;EACjB,MAAM,SAAS,KAAK;EAEpB,IAAI,CAAC,KAAK,cAAc,MAAM,GAC5B,MAAM,IAAI,MAAM,oEAAoE;EAGtF,OAAO,OAAO,sBAAsB,UAAU,OAAO;CACvD;;;;;;;;;CAUA,MAAa,YAA6B;EACxC,MAAM,SAAS,KAAK;EAEpB,IAAI,CAAC,KAAK,cAAc,MAAM,GAC5B,MAAM,IAAI,MAAM,gEAAgE;EAGlF,OAAO,OAAO,UAAU;CAC1B;;;;;;;;;CAUA,MAAa,YAA6B;EACxC,MAAM,SAAS,KAAK;EAEpB,IAAI,CAAC,KAAK,cAAc,MAAM,GAC5B,MAAM,IAAI,MAAM,gEAAgE;EAGlF,OAAO,OAAO,UAAU;CAC1B;;;;;;;;;;CAWA,MAAa,gBAAgB,UAAkB,cAAqC;EAClF,MAAM,SAAS,KAAK;EAEpB,IAAI,CAAC,KAAK,cAAc,MAAM,GAC5B,MAAM,IAAI,MAAM,2DAA2D;EAG7E,OAAO,OAAO,gBAAgB,UAAU,YAAY;CACtD;;;;;;;;;;CAWA,MAAa,cAAc,UAAkB,YAA2C;EACtF,MAAM,SAAS,KAAK;EAEpB,IAAI,CAAC,KAAK,cAAc,MAAM,GAC5B,MAAM,IAAI,MAAM,wDAAwD;EAG1E,OAAO,OAAO,cAAc,UAAU,UAAU;CAClD;;;;;;;;;;CAWA,MAAa,cAAc,UAA2C;EACpE,MAAM,SAAS,KAAK;EAEpB,IAAI,CAAC,KAAK,cAAc,MAAM,GAC5B,MAAM,IAAI,MAAM,wDAAwD;EAG1E,OAAO,OAAO,cAAc,QAAQ;CACtC;;;;;;;;;;CAWA,MAAsB,aAAa,UAAkB,WAAqC;EACxF,OAAO,KAAK,aAAa,aAAa,UAAU,SAAS;CAC3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BA,MAAa,uBAAuB,OAAkD;EAEpF,IACE,EAAE,4BAA4B,KAAK,iBACnC,OAAO,KAAK,aAAa,2BAA2B,YAGpD,OAAO;GACL,OAAO;GACP,OAAO;EACT;EAGF,OAAO,KAAK,aAAa,uBAAuB,KAAK;CACvD;;;;;CAUA,AAAU,aACR,QACgF;EAChF,MAAM,EAAE,QAAQ,GAAG,YAAY;EAE/B,QAAQ,QAAR;GACE,KAAK,SACH,OAAO;IACL,MAAM,QAAQ;IACd,WAAW,QAAQ;IACnB,cAAc,QAAQ;GACxB;GAEF,KAAK;IACH,KAAK,oBAAoB,QAAQ,IAAI;IACrC,OAAO;KACL,GAAG;KACH,QAAQ,QAAQ;KAChB,QAAQ,QAAQ;KAChB,aAAa,QAAQ;KACrB,iBAAiB,QAAQ;KACzB,UAAU,QAAQ;KAClB,WAAW,QAAQ;IACrB;GAEF,KAAK;IACH,KAAK,oBAAoB,QAAQ,IAAI;IACrC,IAAI,CAAC,QAAQ,WACX,MAAM,IAAI,MAAM,gDAA8C;IAGhE,OAAO;KACL,GAAG;KACH,QAAQ,QAAQ,UAAU;KAC1B,QAAQ,QAAQ;KAChB,aAAa,QAAQ;KACrB,iBAAiB,QAAQ;KACzB,UAAU,QAAQ;KAClB,WAAW,QAAQ;KACnB,WAAW,QAAQ;KACnB,cAAc,QAAQ;IACxB;GAEF,KAAK;IACH,KAAK,oBAAoB,QAAQ,QAAQ;IACzC,OAAO;KACL,GAAG;KACH,QAAQ,QAAQ;KAChB,QAAQ,QAAQ;KAChB,aAAa,QAAQ;KACrB,iBAAiB,QAAQ;KACzB,UAAU,QAAQ;KAClB,WAAW,QAAQ;IACrB;GAEF,SACE,MAAM,IAAI,MAAM,wBAAwB,QAAQ;EACpD;CACF;;;;;CAMA,AAAU,oBAAoB,QAA6B,YAA0B;EACnF,MAAM,WAAW;GAAC;GAAU;GAAe;EAAiB;EAE5D,IAAI,eAAe,MACjB,SAAS,KAAK,QAAQ;EAGxB,KAAK,MAAM,SAAS,UAClB,IAAI,CAAC,OAAO,QACV,MAAM,IAAI,MAAM,GAAG,WAAW,YAAY,EAAE,oBAAoB,MAAM,gBAAgB;CAG5F;;;;;CAMA,AAAU,cAAc,MAAqC;EAG3D,IAAI,KAAK,QAAQ,IAAI,IAAI,GACvB,OAAO,KAAK,QAAQ,IAAI,IAAI;EAG9B,MAAM,SAAS,KAAK,QAAQ,IAAI,IAAI;EAEpC,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,mBAAmB,KAAK,oBAAoB;EAG9D,MAAM,UAAU,KAAK,aAAa,MAAM;EACxC,IAAI;EAEJ,QAAQ,OAAO,QAAf;GACE,KAAK;IACH,SAAS,IAAI,YAAY,OAAoC;IAC7D;GACF,KAAK;IACH,SAAS,IAAI,SAAS,OAAoC;IAC1D;GACF,KAAK;IACH,SAAS,IAAI,SAAS,OAAiC;IACvD;GACF,KAAK;IACH,SAAS,IAAI,eAAe,OAAoC;IAChE;GACF,SACE,MAAM,IAAI,MAAM,gCAAgC,OAAO,QAAQ;EACnE;EAEA,KAAK,QAAQ,IAAI,MAAM,MAAM;EAC7B,OAAO;CACT;;;;;CAMA,MAAgB,uBAAmD;EACjE,MAAM,WAAW,cAAc,UAAU;EAEzC,IAAI,UAEF,OAAO,MADgB,SAAS,KACb,KAAK;EAG1B,OAAO,KAAK;CACd;AACF;;;;;;;;;;;;;AAcA,MAAa,UAAU,IAAI,QAAQ"}
@@ -0,0 +1,22 @@
1
+ //#region ../core/src/storage/utils/storage-not-initialized-error.d.ts
2
+ /**
3
+ * Thrown when storage is accessed (e.g. `storage.put(...)`) before it has
4
+ * been initialized in this process — no context driver and no default
5
+ * driver resolved yet.
6
+ *
7
+ * Previously this fell through to a bare `null` `activeDriver`, and the
8
+ * first thing every call site did with it was read `.name` or call a
9
+ * method on it, surfacing as `TypeError: Cannot read properties of null
10
+ * (reading 'name')` deep inside `Storage#put()`. That told the caller
11
+ * nothing about the real cause: the `storage` connector never ran in this
12
+ * process, so `Storage#init()` was never called and `_driver` stayed
13
+ * `null`. This error names that cause and the fix directly instead.
14
+ */
15
+ declare class StorageNotInitializedError extends Error {
16
+ constructor(options?: {
17
+ cause?: unknown;
18
+ });
19
+ }
20
+ //#endregion
21
+ export { StorageNotInitializedError };
22
+ //# sourceMappingURL=storage-not-initialized-error.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-not-initialized-error.d.mts","names":[],"sources":["../../../../../../../../core/src/storage/utils/storage-not-initialized-error.ts"],"mappings":";;AAaA;;;;;;;;;AACkD;;;cADrC,0BAAA,SAAmC,KAAK;cAChC,OAAA;IAAY,KAAA;EAAA;AAAA"}
@@ -0,0 +1,24 @@
1
+ //#region ../core/src/storage/utils/storage-not-initialized-error.ts
2
+ /**
3
+ * Thrown when storage is accessed (e.g. `storage.put(...)`) before it has
4
+ * been initialized in this process — no context driver and no default
5
+ * driver resolved yet.
6
+ *
7
+ * Previously this fell through to a bare `null` `activeDriver`, and the
8
+ * first thing every call site did with it was read `.name` or call a
9
+ * method on it, surfacing as `TypeError: Cannot read properties of null
10
+ * (reading 'name')` deep inside `Storage#put()`. That told the caller
11
+ * nothing about the real cause: the `storage` connector never ran in this
12
+ * process, so `Storage#init()` was never called and `_driver` stayed
13
+ * `null`. This error names that cause and the fix directly instead.
14
+ */
15
+ var StorageNotInitializedError = class extends Error {
16
+ constructor(options) {
17
+ super("Storage is not initialized in this process. This happens when the `storage` connector never ran, so `Storage.init()` was never called.\n\nCheck that `src/config/storage.ts` exists. If this is a custom CLI command, add \"storage\" to its `preload.connectors` list so the connector runs before your command's action executes.", options);
18
+ this.name = "StorageNotInitializedError";
19
+ }
20
+ };
21
+
22
+ //#endregion
23
+ export { StorageNotInitializedError };
24
+ //# sourceMappingURL=storage-not-initialized-error.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-not-initialized-error.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/storage-not-initialized-error.ts"],"sourcesContent":["/**\n * Thrown when storage is accessed (e.g. `storage.put(...)`) before it has\n * been initialized in this process — no context driver and no default\n * driver resolved yet.\n *\n * Previously this fell through to a bare `null` `activeDriver`, and the\n * first thing every call site did with it was read `.name` or call a\n * method on it, surfacing as `TypeError: Cannot read properties of null\n * (reading 'name')` deep inside `Storage#put()`. That told the caller\n * nothing about the real cause: the `storage` connector never ran in this\n * process, so `Storage#init()` was never called and `_driver` stayed\n * `null`. This error names that cause and the fix directly instead.\n */\nexport class StorageNotInitializedError extends Error {\n public constructor(options?: { cause?: unknown }) {\n super(\n \"Storage is not initialized in this process. This happens when the `storage` \" +\n \"connector never ran, so `Storage.init()` was never called.\\n\\n\" +\n \"Check that `src/config/storage.ts` exists. If this is a custom CLI command, \" +\n 'add \"storage\" to its `preload.connectors` list so the connector runs before ' +\n \"your command's action executes.\",\n options,\n );\n\n this.name = \"StorageNotInitializedError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,IAAa,6BAAb,cAAgD,MAAM;CACpD,AAAO,YAAY,SAA+B;EAChD,MACE,uUAKA,OACF;EAEA,KAAK,OAAO;CACd;AACF"}
package/llms-full.txt CHANGED
@@ -964,7 +964,7 @@ Don't call `setBaseUrl` per request — it's process-global and races every othe
964
964
 
965
965
  ---
966
966
  name: configure-app
967
- description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap boot-time refusal on a missing origin — `@warlock.js/sitemap/sitemap-overview/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
967
+ description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
968
968
  ---
969
969
 
970
970
  # Warlock — configure the app
@@ -1224,11 +1224,11 @@ falling back to the `PUBLIC_APP_URL` env var, or `undefined` when neither is
1224
1224
  set.
1225
1225
 
1226
1226
  `getPublicUrl()` never throws — it is a consumer's job to fail loudly when it
1227
- requires the value. `@warlock.js/sitemap` is the first such consumer: with
1228
- `sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set, boot
1229
- refuses to start (`MissingPublicUrlError`, naming both) rather than falling
1230
- back to a request-derived host — a sitemap served from the wrong host is
1231
- worse than one that never boots.
1227
+ requires the value. `@warlock.js/web`'s sitemap is the first such consumer:
1228
+ with `web.sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
1229
+ generation refuses (`MissingPublicUrlError`, naming both) rather than falling
1230
+ back to a request-derived host — a sitemap pointing at the wrong host is
1231
+ worse than none. See `@warlock.js/web/generate-sitemap/SKILL.md`.
1232
1232
 
1233
1233
  ## Common patterns
1234
1234
 
@@ -1309,6 +1309,20 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
1309
1309
 
1310
1310
  Prefer `request.validated()` once a schema is attached — it's typed.
1311
1311
 
1312
+ ### Request body content types
1313
+
1314
+ `request.input()` / `.all()` / `.validated()` read the same way regardless of how the body arrived — every content type below feeds the same parsed bag:
1315
+
1316
+ | Content type | Parsed by | Notes |
1317
+ | ------------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
1318
+ | `application/json` | Fastify (built-in) | objects/arrays parsed as-is |
1319
+ | `multipart/form-data` | `@fastify/multipart` | fields + files; see [`upload-file`](../upload-file/SKILL.md) |
1320
+ | `application/x-www-form-urlencoded` | Warlock's own content-type parser (`http/parse-urlencoded-body.ts`) | plain HTML forms, OAuth `form_post` callbacks (e.g. Apple Sign in) |
1321
+
1322
+ For urlencoded bodies: fields decode via `URLSearchParams`. A key sent more than once (`tag=a&tag=b`) becomes an array (`request.input("tag")` → `["a", "b"]`); every other key is a plain string. Bracket-notation keys (`a[b]=1`) are **not** expanded by the urlencoded parser itself — nesting only happens through the same shared bracket-key logic every body type already goes through, so it behaves exactly like a JSON or query-string key of that shape, no differently than today.
1323
+
1324
+ All three content types are held to the same `http.bodyLimit` — an over-limit urlencoded body is rejected with the same `413` a JSON body would get.
1325
+
1312
1326
  ## Returning output
1313
1327
 
1314
1328
  Pick the helper that matches the outcome. Full surface in [send-response](../send-response/SKILL.md). Quick map:
@@ -2416,6 +2430,8 @@ GET /ready → 200 {"status":"ok","checks":{"db":true}}
2416
2430
  ### Config
2417
2431
 
2418
2432
  ```ts title="src/config/http.ts"
2433
+ import type { HttpConfigurations } from "@warlock.js/core";
2434
+
2419
2435
  const httpConfigurations: HttpConfigurations = {
2420
2436
  health: {
2421
2437
  enabled: true, // default; set false to remove both endpoints
@@ -2423,6 +2439,8 @@ const httpConfigurations: HttpConfigurations = {
2423
2439
  readinessPath: "/ready", // readiness path
2424
2440
  },
2425
2441
  };
2442
+
2443
+ export default httpConfigurations;
2426
2444
  ```
2427
2445
 
2428
2446
  ## Readiness checks
@@ -2463,12 +2481,16 @@ On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks
2463
2481
  3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
2464
2482
 
2465
2483
  ```ts title="src/config/http.ts"
2484
+ import type { HttpConfigurations } from "@warlock.js/core";
2485
+
2466
2486
  const httpConfigurations: HttpConfigurations = {
2467
2487
  gracefulShutdown: {
2468
2488
  timeout: 10_000, // ms to wait for in-flight drain (default 10s)
2469
2489
  forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
2470
2490
  },
2471
2491
  };
2492
+
2493
+ export default httpConfigurations;
2472
2494
  ```
2473
2495
 
2474
2496
  `forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
@@ -4489,6 +4511,8 @@ return response.sendBuffer(buffer, { contentType: "image/png" });
4489
4511
 
4490
4512
  `SendFileOptions` lets you set `cacheTime`, `immutable`, `inline`, `filename` (download attachment name).
4491
4513
 
4514
+ `response.xml(string | XMLable, statusCode?)` sends `application/xml` — pass a raw XML string, or anything with a `toXML(): string` method (structural, so core never depends on `@warlock.js/sitemap`). This is for BOUNDED values only: a raw string, or a single `@warlock.js/sitemap` `Sitemap` under the sitemaps.org 50,000-URL / 50MB ceiling. `SitemapIndex` has no `toXML()` — it is the streaming path for larger URL sets and does not fit in one response body. It writes shards plus a master index as FILES via `saveTo`/`publish`, and those files are served directly rather than passed through `xml()`. In Warlock web this is the `web.sitemap` config: `enabled`, `outputDir` (default `storagePath("sitemap")`), and `path` (default `/sitemap.xml`) — the app serves the generated files from `outputDir` at that route, it never builds a `SitemapIndex` response inline.
4515
+
4492
4516
  ## Streams
4493
4517
 
4494
4518
  ```ts
@@ -6905,6 +6929,22 @@ GET /products/42?locale=ar
6905
6929
  - [`create-module/SKILL.md`](../create-module/SKILL.md) — the `utils/locales.ts` file is part of the generated module scaffold.
6906
6930
  - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — `utils/locales.ts` is auto-loaded; the suffix is mandatory.
6907
6931
 
6932
+ ## Route-owned web translations (5.17)
6933
+
6934
+ `locales.json` applies only to descendant pages on its physical `src/web` ancestor chain. Its optional `$group` overrides the namespace; otherwise the physical directory supplies it, omitting `(group)` and `[param]` segments. Root `src/web/locales.json` has no implicit prefix. Nested objects flatten to dotted keys, and all duplicate flattened key ownership is rejected, including matching ancestor/child declarations.
6935
+
6936
+ ```json
6937
+ {
6938
+ "$group": "account",
6939
+ "title": { "en": "Account", "ar": "Arabic account" },
6940
+ "actions": { "save": { "en": "Save", "ar": "Arabic save" } }
6941
+ }
6942
+ ```
6943
+
6944
+ This produces `account.title` and `account.actions.save`. Keep every leaf complete for the configured locale list. The page pipeline selects an immutable snapshot for its route source and locale. `useTrans()`, `request.t()`, and `request.trans()` use that snapshot; a missing scoped key returns the key itself. With no route-locales manifest, the existing global `@mongez/localization` registry remains the legacy fallback path.
6945
+
6946
+ When a route-locales snapshot applies, `request.t()`, `request.trans()`, and `request.transFrom(locale, key, placeholders?)` resolve through it; without one, they retain the existing global-registry behavior. The underlying `transFromKeywords(locale, keywords, key, placeholders?, converter?)` is supplied by `@mongez/localization` 3.5.0. Error pages rebind to their own snapshot, while framework fallback uses the app/root snapshot. Development HMR add/edit/delete publishes a new snapshot, and the snapshot revision keeps page-cache entries from reusing old JSON. Generated `.warlock` typings contain the flattened route keys; do not maintain a second handwritten key union.
6947
+
6908
6948
 
6909
6949
  ## use-middleware `@warlock.js/core/use-middleware/SKILL.md`
6910
6950
 
@@ -7086,7 +7126,7 @@ router.get("/analytics/summary", summaryController, {
7086
7126
 
7087
7127
  ### Tag-based invalidation
7088
7128
 
7089
- Give it `tags` — a static list, or a function of the request — to evict the entry early with `cache.tags([...]).invalidate()` (`@warlock.js/cache`), instead of waiting out `ttl`. Mirrors `route.cache.tags` on `@warlock.js/web`'s page cache, so an API response and a page can share the same tag and be invalidated together:
7129
+ Give it `tags` — a static list, or a function of the request — to evict the entry early with `cache.tags([...]).invalidate()` (`@warlock.js/cache`), instead of waiting out `ttl`. Mirrors `config.cache.tags` on `@warlock.js/web`'s page cache, so an API response and a page can share the same tag and be invalidated together:
7090
7130
 
7091
7131
  ```ts
7092
7132
  import { middleware } from "@warlock.js/core";
package/llms.txt CHANGED
@@ -10,7 +10,7 @@
10
10
  - [benchmark-code](@warlock.js/core/benchmark-code/SKILL.md): Wrap a function with `measure(name, fn, options?)` to time it and classify the latency — onComplete/onError/onFinish hooks, `latencyRange` thresholds, `BenchmarkProfiler` for percentiles, `BenchmarkSnapshots` for raw captures. Triggers: `measure`, `BenchmarkProfiler`, `BenchmarkSnapshots`, `BenchmarkChannel`, `ConsoleChannel`, `latencyRange`, `shouldBenchmarkError`; "time this operation", "profile a slow service", "emit p50/p95/p99 metrics", "classify latency against thresholds"; typical import `import { measure, BenchmarkProfiler } from "@warlock.js/core"`. Skip: retry composition — `@warlock.js/core/retry-operation/SKILL.md`; benchmark config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `prom-client`, `pino`, `perf_hooks`, `console.time`.
11
11
  - [build-restful](@warlock.js/core/build-restful/SKILL.md): Generate standard CRUD endpoints — via `router.route(path).list().show().create().update().destroy()` chain or the `Restful` base class. Pick the chain by default; reach for `Restful` when you want repository-bound defaults. Triggers: `router.route`, `Restful`, `router.restfulResource`, `RouteResource`, `.crud`, `.nest`, `beforeCreate`, `onCreate`; "build a CRUD API", "register list/show/create/update/destroy", "repository-bound default handlers", "override a single REST action"; typical import `import { router, Restful } from "@warlock.js/core"`. Skip: wider router surface — `@warlock.js/core/register-route/SKILL.md`; per-action controllers — `@warlock.js/core/create-controller/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing pattern: hand-rolled controllers, `@nestjs/swagger` decorator-driven CRUD.
12
12
  - [build-url](@warlock.js/core/build-url/SKILL.md): HTTP URL helpers — `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, anchored at `app.baseUrl`. Use to render `src` / `href` / API URLs in resources and responses. `setBaseUrl` is wired by the HTTP connector from `config.get("app.baseUrl")`. Triggers: `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, `setBaseUrl`, `BASE_URL`; "render an avatar src URL", "absolute download link", "embed asset URL in email", "URL helpers vs path helpers"; typical import `import { url, publicUrl, uploadsUrl } from "@warlock.js/core"`. Skip: filesystem paths — `@warlock.js/core/resolve-path/SKILL.md`; signed CDN URLs — `@warlock.js/core/store-file/SKILL.md`; resource output — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: hand-rolled `${baseUrl}/...` template strings.
13
- - [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app's public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap boot-time refusal on a missing origin — `@warlock.js/sitemap/sitemap-overview/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.
13
+ - [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app's public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.
14
14
  - [create-controller](@warlock.js/core/create-controller/SKILL.md): Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.
15
15
  - [create-module](@warlock.js/core/create-module/SKILL.md): Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `npx warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.
16
16
  - [define-resource](@warlock.js/core/define-resource/SKILL.md): Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — `@warlock.js/core/use-localization/SKILL.md`; URL casting — `@warlock.js/core/build-url/SKILL.md`; controller side — `@warlock.js/core/create-controller/SKILL.md`; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.
package/package.json CHANGED
@@ -20,46 +20,47 @@
20
20
  "@mongez/dotenv": "^1.3.2",
21
21
  "@mongez/events": "^2.2.7",
22
22
  "@mongez/http": "^3.5.0",
23
- "@mongez/localization": "^3.4.7",
23
+ "@mongez/localization": "^3.5.0",
24
24
  "@mongez/reinforcements": "^4.0.1",
25
25
  "@mongez/slug": "^1.0.7",
26
26
  "@mongez/supportive-is": "^2.1.4",
27
27
  "@mongez/time-wizard": "^1.0.6",
28
- "@warlock.js/cache": "5.15.0",
29
- "@warlock.js/cascade": "5.15.0",
30
- "@warlock.js/context": "5.15.0",
31
- "@warlock.js/logger": "5.15.0",
32
- "@warlock.js/seal": "5.15.0",
33
- "@warlock.js/fs": "5.15.0",
28
+ "@warlock.js/cache": "5.17.0",
29
+ "@warlock.js/cascade": "5.17.0",
30
+ "@warlock.js/context": "5.17.0",
31
+ "@warlock.js/fs": "5.17.0",
32
+ "@warlock.js/logger": "5.17.0",
33
+ "@warlock.js/seal": "5.17.0",
34
+ "bcryptjs": "^3.0.3",
34
35
  "chokidar": "^5.0.0",
35
36
  "dayjs": "^1.11.19",
36
37
  "es-module-lexer": "^2.0.0",
37
38
  "esbuild": "^0.27.2",
39
+ "eslint": "^9.18.0",
38
40
  "fast-glob": "^3.3.3",
39
41
  "fastify": "^5.6.2",
40
42
  "find-my-way": "^9.3.0",
41
- "eslint": "^9.18.0",
42
- "typescript": "^5.7.3",
43
+ "get-tsconfig": "^5.0.0-beta.5",
44
+ "import-meta-resolve": "^4.2.0",
43
45
  "mime": "^4.1.0",
44
- "bcryptjs": "^3.0.3",
45
46
  "pluralize-esm": "^9.0.5",
46
- "get-tsconfig": "^5.0.0-beta.5"
47
+ "typescript": "^5.7.3"
47
48
  },
48
49
  "peerDependencies": {
49
- "sharp": "^0.34.5",
50
- "socket.io": "^4.8.3",
51
- "nodemailer": "^8.0.5",
52
50
  "@aws-sdk/client-s3": "^3.955.0",
51
+ "@aws-sdk/client-sesv2": "^3.1025.0",
53
52
  "@aws-sdk/lib-storage": "^3.955.0",
54
53
  "@aws-sdk/s3-request-presigner": "^3.955.0",
55
- "@aws-sdk/client-sesv2": "^3.1025.0",
54
+ "@react-email/render": "^2.0.5",
55
+ "@warlock.js/access": "5.17.0",
56
+ "@warlock.js/ai": "5.17.0",
57
+ "@warlock.js/herald": "5.17.0",
58
+ "@warlock.js/notifications": "5.17.0",
59
+ "nodemailer": "^8.0.5",
56
60
  "react": "^19.2.3",
57
61
  "react-dom": "^19.2.3",
58
- "@react-email/render": "^2.0.5",
59
- "@warlock.js/herald": "5.15.0",
60
- "@warlock.js/ai": "5.15.0",
61
- "@warlock.js/access": "5.15.0",
62
- "@warlock.js/notifications": "5.15.0"
62
+ "sharp": "^0.34.5",
63
+ "socket.io": "^4.8.3"
63
64
  },
64
65
  "peerDependenciesMeta": {
65
66
  "sharp": {
@@ -122,7 +123,7 @@
122
123
  ],
123
124
  "author": "hassanzohdy",
124
125
  "license": "MIT",
125
- "version": "5.15.0",
126
+ "version": "5.17.0",
126
127
  "type": "module",
127
128
  "main": "./esm/index.mjs",
128
129
  "module": "./esm/index.mjs",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: configure-app
3
- description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap boot-time refusal on a missing origin — `@warlock.js/sitemap/sitemap-overview/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
3
+ description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
4
4
  ---
5
5
 
6
6
  # Warlock — configure the app
@@ -260,11 +260,11 @@ falling back to the `PUBLIC_APP_URL` env var, or `undefined` when neither is
260
260
  set.
261
261
 
262
262
  `getPublicUrl()` never throws — it is a consumer's job to fail loudly when it
263
- requires the value. `@warlock.js/sitemap` is the first such consumer: with
264
- `sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set, boot
265
- refuses to start (`MissingPublicUrlError`, naming both) rather than falling
266
- back to a request-derived host — a sitemap served from the wrong host is
267
- worse than one that never boots.
263
+ requires the value. `@warlock.js/web`'s sitemap is the first such consumer:
264
+ with `web.sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
265
+ generation refuses (`MissingPublicUrlError`, naming both) rather than falling
266
+ back to a request-derived host — a sitemap pointing at the wrong host is
267
+ worse than none. See `@warlock.js/web/generate-sitemap/SKILL.md`.
268
268
 
269
269
  ## Common patterns
270
270
 
@@ -41,6 +41,20 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
41
41
 
42
42
  Prefer `request.validated()` once a schema is attached — it's typed.
43
43
 
44
+ ### Request body content types
45
+
46
+ `request.input()` / `.all()` / `.validated()` read the same way regardless of how the body arrived — every content type below feeds the same parsed bag:
47
+
48
+ | Content type | Parsed by | Notes |
49
+ | ------------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
50
+ | `application/json` | Fastify (built-in) | objects/arrays parsed as-is |
51
+ | `multipart/form-data` | `@fastify/multipart` | fields + files; see [`upload-file`](../upload-file/SKILL.md) |
52
+ | `application/x-www-form-urlencoded` | Warlock's own content-type parser (`http/parse-urlencoded-body.ts`) | plain HTML forms, OAuth `form_post` callbacks (e.g. Apple Sign in) |
53
+
54
+ For urlencoded bodies: fields decode via `URLSearchParams`. A key sent more than once (`tag=a&tag=b`) becomes an array (`request.input("tag")` → `["a", "b"]`); every other key is a plain string. Bracket-notation keys (`a[b]=1`) are **not** expanded by the urlencoded parser itself — nesting only happens through the same shared bracket-key logic every body type already goes through, so it behaves exactly like a JSON or query-string key of that shape, no differently than today.
55
+
56
+ All three content types are held to the same `http.bodyLimit` — an over-limit urlencoded body is rejected with the same `413` a JSON body would get.
57
+
44
58
  ## Returning output
45
59
 
46
60
  Pick the helper that matches the outcome. Full surface in [send-response](../send-response/SKILL.md). Quick map: