@warlock.js/core 4.10.0 → 4.11.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.
- package/CHANGELOG.md +45 -0
- package/esm/application/application.d.mts +19 -0
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +25 -0
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/boot-signal.d.mts +77 -0
- package/esm/application/boot-signal.d.mts.map +1 -0
- package/esm/application/boot-signal.mjs +64 -0
- package/esm/application/boot-signal.mjs.map +1 -0
- package/esm/application/index.d.mts +1 -0
- package/esm/application/index.mjs +1 -0
- package/esm/bootstrap.mjs +2 -2
- package/esm/bootstrap.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +3 -3
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs +57 -1
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +4 -25
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/types.d.mts +6 -1
- package/esm/cli/types.d.mts.map +1 -1
- package/esm/config/config-getter.mjs +5 -5
- package/esm/config/config-getter.mjs.map +1 -1
- package/esm/config/config-loader.mjs +2 -2
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/config-setter.d.mts +27 -0
- package/esm/config/config-setter.d.mts.map +1 -0
- package/esm/config/config-setter.mjs +10 -0
- package/esm/config/config-setter.mjs.map +1 -0
- package/esm/config/index.mjs +1 -0
- package/esm/connectors/access-connector.mjs +2 -2
- package/esm/connectors/access-connector.mjs.map +1 -1
- package/esm/connectors/ai-connector.mjs +2 -2
- package/esm/connectors/ai-connector.mjs.map +1 -1
- package/esm/connectors/cache-connector.mjs +2 -2
- package/esm/connectors/cache-connector.mjs.map +1 -1
- package/esm/connectors/database-connector.mjs +2 -2
- package/esm/connectors/database-connector.mjs.map +1 -1
- package/esm/connectors/herald-connector.mjs +2 -2
- package/esm/connectors/herald-connector.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +8 -7
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/logger-connector.mjs +2 -2
- package/esm/connectors/logger-connector.mjs.map +1 -1
- package/esm/connectors/mail-connector.mjs +2 -2
- package/esm/connectors/mail-connector.mjs.map +1 -1
- package/esm/connectors/notifications-connector.mjs +2 -2
- package/esm/connectors/notifications-connector.mjs.map +1 -1
- package/esm/connectors/socket-connector.mjs +3 -3
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/storage.connector.d.mts +11 -5
- package/esm/connectors/storage.connector.d.mts.map +1 -1
- package/esm/connectors/storage.connector.mjs +11 -5
- package/esm/connectors/storage.connector.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +1 -1
- package/esm/generations/stubs.mjs +1 -1
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/config.mjs +2 -2
- package/esm/http/config.mjs.map +1 -1
- package/esm/http/createHttpApplication.mjs +2 -2
- package/esm/http/createHttpApplication.mjs.map +1 -1
- package/esm/http/index.d.mts +1 -0
- package/esm/http/index.mjs +1 -0
- package/esm/http/middleware/idempotency.middleware.mjs +5 -5
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +2 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +4 -4
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/plugins.mjs +9 -9
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/port-preflight.d.mts +34 -0
- package/esm/http/port-preflight.d.mts.map +1 -0
- package/esm/http/port-preflight.mjs +66 -0
- package/esm/http/port-preflight.mjs.map +1 -0
- package/esm/http/response.mjs +5 -5
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.mjs +4 -4
- package/esm/http/server.mjs.map +1 -1
- package/esm/index.d.mts +6 -2
- package/esm/index.mjs +5 -1
- package/esm/production/assert-generated-imports.mjs +94 -0
- package/esm/production/assert-generated-imports.mjs.map +1 -0
- package/esm/production/production-builder.mjs +29 -4
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +108 -0
- package/esm/production/production-supervisor.mjs.map +1 -0
- package/esm/storage/storage.d.mts +16 -0
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +24 -1
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/tests/index.d.mts +2 -1
- package/esm/tests/index.mjs +1 -0
- package/esm/tests/start-http-development-server.d.mts +15 -2
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +43 -16
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-helpers.d.mts.map +1 -1
- package/esm/tests/test-helpers.mjs +2 -1
- package/esm/tests/test-helpers.mjs.map +1 -1
- package/esm/tests/test-server-port-channel.d.mts +27 -0
- package/esm/tests/test-server-port-channel.d.mts.map +1 -0
- package/esm/tests/test-server-port-channel.mjs +32 -0
- package/esm/tests/test-server-port-channel.mjs.map +1 -0
- package/esm/utils/load-environment.mjs +59 -0
- package/esm/utils/load-environment.mjs.map +1 -0
- package/esm/utils/paths.mjs +2 -2
- package/esm/utils/paths.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +2 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +90 -3
- package/llms.txt +1 -1
- package/package.json +13 -13
- package/skills/run-app/SKILL.md +63 -0
- package/skills/test-http/SKILL.md +27 -3
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { appPath, warlockPath } from "../utils/paths.mjs";
|
|
1
|
+
import { appPath, rootPath, warlockPath } from "../utils/paths.mjs";
|
|
2
2
|
import "../utils/index.mjs";
|
|
3
3
|
import { tsconfigManager } from "../dev-server/tsconfig-manager.mjs";
|
|
4
|
+
import { assertGeneratedImportsAreDeclared } from "./assert-generated-imports.mjs";
|
|
4
5
|
import { nativeNodeModulesPlugin } from "./esbuild-plugins.mjs";
|
|
5
6
|
import { resolveBuildConfig } from "./resolve-build-config.mjs";
|
|
6
7
|
import path from "path";
|
|
7
8
|
import { toCamelCase } from "@mongez/reinforcements";
|
|
8
9
|
import { colors } from "@mongez/copper";
|
|
9
|
-
import { ensureDirectoryAsync, fileExistsAsync, putFileAsync, removeDirectoryAsync } from "@warlock.js/fs";
|
|
10
|
+
import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, getJsonFileAsync, putFileAsync, removeDirectoryAsync } from "@warlock.js/fs";
|
|
10
11
|
import glob from "fast-glob";
|
|
11
12
|
import esbuild from "esbuild";
|
|
12
13
|
|
|
@@ -34,6 +35,7 @@ var ProductionBuilder = class {
|
|
|
34
35
|
await this.initializeOptions();
|
|
35
36
|
await this.generateCombinedFiles();
|
|
36
37
|
await this.generateEntryPoint();
|
|
38
|
+
await this.assertGeneratedImports();
|
|
37
39
|
await this.bundle();
|
|
38
40
|
await removeDirectoryAsync(this.productionDir);
|
|
39
41
|
console.log(colors.green("Build complete!"));
|
|
@@ -83,6 +85,29 @@ bootstrap();
|
|
|
83
85
|
await putFileAsync(path.join(this.productionDir, "bootstrap.ts"), content);
|
|
84
86
|
}
|
|
85
87
|
/**
|
|
88
|
+
* Fail the build when generated code imports a package the app does not
|
|
89
|
+
* declare.
|
|
90
|
+
*
|
|
91
|
+
* The bundler is configured `packages: "external"`, so every bare specifier
|
|
92
|
+
* written above survives verbatim into an artifact that resolves against the
|
|
93
|
+
* **consuming app's** node_modules. A framework-internal dependency imported
|
|
94
|
+
* there resolves by accident under npm/yarn hoisting and dies under pnpm —
|
|
95
|
+
* which is how a production app shipped that could not boot. Rewriting the
|
|
96
|
+
* offending import fixes today's bundle; this check is what stops the next
|
|
97
|
+
* change to the generator from quietly reintroducing it.
|
|
98
|
+
*/
|
|
99
|
+
async assertGeneratedImports() {
|
|
100
|
+
const generatedPaths = await glob("**/*.{ts,tsx,js,mjs}", {
|
|
101
|
+
cwd: this.productionDir,
|
|
102
|
+
absolute: false
|
|
103
|
+
});
|
|
104
|
+
const [files, appPackage] = await Promise.all([Promise.all(generatedPaths.map(async (file) => ({
|
|
105
|
+
file,
|
|
106
|
+
content: await getFileAsync(path.join(this.productionDir, file))
|
|
107
|
+
}))), getJsonFileAsync(rootPath("package.json"))]);
|
|
108
|
+
assertGeneratedImportsAreDeclared(files, Object.keys(appPackage.dependencies ?? {}));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
86
111
|
* Glob for module files matching a pattern
|
|
87
112
|
* Returns relative paths from .warlock/production/ to src/app/
|
|
88
113
|
*/
|
|
@@ -110,7 +135,7 @@ bootstrap();
|
|
|
110
135
|
cwd: path.join(process.cwd(), "src/config"),
|
|
111
136
|
absolute: false
|
|
112
137
|
})).map((f) => f.replace(/\.(ts|tsx)$/, ""));
|
|
113
|
-
const imports = ["import
|
|
138
|
+
const imports = ["import { setConfig, configSpecialHandlers } from \"@warlock.js/core\";"];
|
|
114
139
|
const configImports = [];
|
|
115
140
|
const configSetCalls = [];
|
|
116
141
|
const executors = [];
|
|
@@ -118,7 +143,7 @@ bootstrap();
|
|
|
118
143
|
const properConfigName = toCamelCase(configName);
|
|
119
144
|
const varName = `${properConfigName}Config`;
|
|
120
145
|
configImports.push(`import ${varName} from "../../src/config/${configName}";`);
|
|
121
|
-
configSetCalls.push(`
|
|
146
|
+
configSetCalls.push(`setConfig("${properConfigName}", ${varName});`);
|
|
122
147
|
executors.push(`await configSpecialHandlers.execute("${properConfigName}", ${varName});`);
|
|
123
148
|
}
|
|
124
149
|
let content = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"production-builder.mjs","names":[],"sources":["../../../../../../../core/src/production/production-builder.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n putFileAsync,\r\n removeDirectoryAsync,\r\n} from \"@warlock.js/fs\";\r\nimport esbuild from \"esbuild\";\r\nimport glob from \"fast-glob\";\r\nimport path from \"path\";\r\nimport { tsconfigManager } from \"../dev-server/tsconfig-manager\";\r\nimport { appPath, warlockPath } from \"../utils\";\r\nimport { nativeNodeModulesPlugin } from \"./esbuild-plugins\";\r\nimport { resolveBuildConfig, type ResolvedBuildConfig } from \"./resolve-build-config\";\r\nimport { toCamelCase, toKebabCase } from \"@mongez/reinforcements\";\r\n\r\n/**\r\n * Production Builder\r\n * Generates production-ready files and bundles them for deployment\r\n * Build options are loaded from warlock.config.ts\r\n */\r\nexport class ProductionBuilder {\r\n private options!: ResolvedBuildConfig;\r\n private readonly productionDir = warlockPath(\"production\");\r\n\r\n /**\r\n * Main build entry point\r\n */\r\n public async build(): Promise<void> {\r\n console.log(colors.cyan(\"Building for production...\\n\"));\r\n\r\n // Step 1: Initialize options from config\r\n await this.initializeOptions();\r\n\r\n // Step 2: Generate combined files\r\n await this.generateCombinedFiles();\r\n\r\n // Step 3: Generate entry point\r\n await this.generateEntryPoint();\r\n\r\n // Step 4: Bundle with esbuild\r\n await this.bundle();\r\n\r\n // // Step 5: Remove production folder\r\n await removeDirectoryAsync(this.productionDir);\r\n\r\n console.log(colors.green(\"Build complete!\"));\r\n console.log(`Start production server by running ${colors.cyan(\"warlock start\")}`);\r\n }\r\n\r\n /**\r\n * Initialize options from warlock.config.ts\r\n */\r\n private async initializeOptions(): Promise<void> {\r\n this.options = resolveBuildConfig();\r\n\r\n // Ensure production directory exists\r\n await ensureDirectoryAsync(this.productionDir);\r\n }\r\n\r\n /**\r\n * Track which special files were generated\r\n */\r\n private generatedFiles = {\r\n locales: false,\r\n events: false,\r\n main: false,\r\n routes: false,\r\n };\r\n\r\n /**\r\n * Generate all combined files\r\n */\r\n private async generateCombinedFiles(): Promise<void> {\r\n console.log(colors.yellow(\" Generating production files...\"));\r\n\r\n // Generate bootstrap.ts file\r\n await this.generateBootstrap();\r\n\r\n // Generate config loader\r\n await this.generateConfigLoader();\r\n\r\n // Generate special files and track which ones have content\r\n const [locales, events, main, routes] = await Promise.all([\r\n this.generateLocales(),\r\n this.generateEvents(),\r\n this.generateMain(),\r\n this.generateRoutes(),\r\n ]);\r\n\r\n this.generatedFiles = { locales, events, main, routes };\r\n }\r\n\r\n /**\r\n * Generate bootstrap.ts - ensures bootstrap() runs first and sets production environment\r\n */\r\n private async generateBootstrap(): Promise<void> {\r\n let content = `import { bootstrap, Application } from \"@warlock.js/core\";\r\n\r\n// Set production environment\r\nApplication.setRuntimeStrategy(\"production\");\r\nApplication.setEnvironment(\"production\");\r\n\r\n// Bootstrap the application\r\nbootstrap();\r\n`;\r\n if (await fileExistsAsync(appPath(\"bootstrap.ts\"))) {\r\n content += \"import './../../src/app/bootstrap';\\n\";\r\n }\r\n\r\n await putFileAsync(path.join(this.productionDir, \"bootstrap.ts\"), content);\r\n }\r\n\r\n /**\r\n * Glob for module files matching a pattern\r\n * Returns relative paths from .warlock/production/ to src/app/\r\n */\r\n private async globModule(fileName: string): Promise<string[]> {\r\n const pattern = `**/${fileName}.{ts,tsx}`;\r\n const appDirectory = appPath();\r\n\r\n const files = await glob(pattern, {\r\n cwd: appDirectory,\r\n absolute: false,\r\n });\r\n\r\n // Convert to relative paths from .warlock/production/ to src/app/\r\n // e.g., \"users/main\" -> \"../../src/app/users/main\"\r\n return files.map((file) => \"../../src/app/\" + file.replace(/\\.(ts|tsx)$/, \"\"));\r\n }\r\n\r\n /**\r\n * Glob for files in a specific directory pattern\r\n * Returns relative paths from .warlock/production/ to src/app/\r\n */\r\n private async globModuleDirectory(directory: string): Promise<string[]> {\r\n const pattern = `**/${directory}/*.{ts,tsx}`;\r\n const appDirectory = appPath();\r\n\r\n const files = await glob(pattern, {\r\n cwd: appDirectory,\r\n absolute: false,\r\n });\r\n\r\n return files.map((file) => \"../../src/app/\" + file.replace(/\\.(ts|tsx)$/, \"\"));\r\n }\r\n\r\n /**\r\n * Generate config-loader.ts\r\n */\r\n private async generateConfigLoader(): Promise<void> {\r\n const configDirectory = path.join(process.cwd(), \"src/config\");\r\n\r\n const files = await glob(\"*.{ts,tsx}\", {\r\n cwd: configDirectory,\r\n absolute: false,\r\n });\r\n\r\n const configNames = files.map((f) => f.replace(/\\.(ts|tsx)$/, \"\"));\r\n\r\n const imports: string[] = [\r\n 'import config from \"@mongez/config\";',\r\n 'import { configSpecialHandlers } from \"@warlock.js/core\";',\r\n ];\r\n const configImports: string[] = [];\r\n const configSetCalls: string[] = [];\r\n const executors: string[] = [];\r\n\r\n for (const configName of configNames) {\r\n const properConfigName = toCamelCase(configName);\r\n const varName = `${properConfigName}Config`;\r\n configImports.push(`import ${varName} from \"../../src/config/${configName}\";`);\r\n configSetCalls.push(`config.set(\"${properConfigName}\", ${varName});`);\r\n executors.push(`await configSpecialHandlers.execute(\"${properConfigName}\", ${varName});`);\r\n }\r\n\r\n let content = [\r\n ...imports,\r\n \"\",\r\n \"// Config imports\",\r\n ...configImports,\r\n \"\",\r\n \"// Register configs\",\r\n ...configSetCalls,\r\n \"\",\r\n \"// Special handlers\",\r\n ...executors,\r\n \"\",\r\n ].join(\"\\n\");\r\n\r\n if (await fileExistsAsync(appPath(\"prestart.ts\"))) {\r\n content += \"import './../../src/app/prestart';\\n\";\r\n }\r\n\r\n await putFileAsync(path.join(this.productionDir, \"config-loader.ts\"), content);\r\n }\r\n\r\n /**\r\n * Generate locales.ts (only if there are locale files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateLocales(): Promise<boolean> {\r\n const files = await this.globModule(\"utils/locales\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"locales.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate events.ts (only if there are event files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateEvents(): Promise<boolean> {\r\n const files = await this.globModuleDirectory(\"events\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"events.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate main.ts (only if there are main files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateMain(): Promise<boolean> {\r\n const files = await this.globModule(\"main\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"main.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate routes.ts (only if there are route files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateRoutes(): Promise<boolean> {\r\n const files = await this.globModule(\"routes\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"routes.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate a file with imports from all given files\r\n */\r\n private async generateImportsFile(importPaths: string[], outputFile: string): Promise<void> {\r\n const imports = importPaths.map((importPath) => `import \"${importPath}\";`);\r\n const content = imports.join(\"\\n\") + \"\\n\";\r\n await putFileAsync(path.join(this.productionDir, outputFile), content);\r\n }\r\n\r\n /**\r\n * Generate the main entry point (app.ts)\r\n */\r\n private async generateEntryPoint(): Promise<void> {\r\n console.log(colors.yellow(\" Generating entry point...\"));\r\n\r\n // Build imports based on which files were generated\r\n const imports: string[] = [\r\n \"// 1. Bootstrap (loads .env, initializes framework)\",\r\n 'import \"./bootstrap\";',\r\n \"\",\r\n \"// 2. Load configs\",\r\n 'import \"./config-loader\";',\r\n \"\",\r\n \"// 3. Start early-phase connectors (database, cache, logger, ...)\",\r\n \"// so data sources, cache, etc. are ready before app code runs\",\r\n 'import { Application, connectorsManager, ConnectorLifecyclePhase } from \"@warlock.js/core\";',\r\n \"await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\",\r\n ];\r\n\r\n // App code uses dynamic `await import(...)` so each module's side\r\n // effects fire at THIS point in execution. Static imports would be\r\n // hoisted to module-instantiation time (before the early-phase await\r\n // resolves), which is exactly the bug this split is meant to fix.\r\n // Requires `splitting: true` in esbuild (set below in bundle()).\r\n imports.push(\"\", \"// 4. Load app code (events, locales, main, routes)\");\r\n\r\n if (this.generatedFiles.events) {\r\n imports.push('await import(\"./events\");');\r\n }\r\n if (this.generatedFiles.locales) {\r\n imports.push('await import(\"./locales\");');\r\n }\r\n if (this.generatedFiles.main) {\r\n imports.push('await import(\"./main\");');\r\n }\r\n if (this.generatedFiles.routes) {\r\n imports.push('await import(\"./routes\");');\r\n }\r\n\r\n // Start late-phase connectors after app code registers routes/listeners\r\n imports.push(\r\n \"\",\r\n \"// 5. Start late-phase connectors (http, socket) — routes and\",\r\n \"// listeners registered by app code are now ready to bind\",\r\n \"await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\",\r\n \"\",\r\n \"// 6. Signal a complete boot so `Application.onceBooted(...)` listeners fire\",\r\n \"Application.markBooted({ environment: Application.environment, runtimeStrategy: Application.runtimeStrategy });\",\r\n \"connectorsManager.shutdownOnProcessKill();\",\r\n );\r\n\r\n const content = imports.join(\"\\n\") + \"\\n\";\r\n await putFileAsync(path.join(this.productionDir, \"app.ts\"), content);\r\n }\r\n\r\n /**\r\n * Bundle with esbuild\r\n */\r\n private async bundle(): Promise<void> {\r\n console.log(colors.magenta(\" Bundling with esbuild...\"));\r\n\r\n const entryPoint = path.join(this.productionDir, \"app.ts\");\r\n const outDir = this.options.outdir!;\r\n const outFileName = this.options.outFile!;\r\n // Strip extension so entryNames produces \"<base>.js\" via esbuild\r\n const entryName = path.basename(outFileName, path.extname(outFileName));\r\n\r\n delete this.options.outFile;\r\n delete this.options.entryPath;\r\n\r\n await ensureDirectoryAsync(outDir);\r\n\r\n const alias = this.buildAliasMapFromTsconfig();\r\n\r\n\r\n try {\r\n \r\n await esbuild.build({\r\n platform: \"node\",\r\n entryPoints: [entryPoint],\r\n bundle: true,\r\n // Required so dynamic `await import(\"./main\")` in the generated\r\n // app.ts produces separate chunks loaded at the runtime call site,\r\n // instead of inlining the modules at instantiation time (which\r\n // would defeat the early/late phase split).\r\n splitting: true,\r\n packages: \"external\",\r\n minify: this.options!.minify,\r\n sourcemap: this.options!.sourcemap === true ? \"linked\" : this.options!.sourcemap,\r\n format: \"esm\",\r\n // Targeting a concrete Node version (not \"esnext\") so esbuild\r\n // transpiles TC39 stage 3 decorators into helpers — Node does not\r\n // implement them natively yet.\r\n target: [\"node22\"],\r\n outdir: outDir,\r\n entryNames: entryName,\r\n alias,\r\n plugins: [nativeNodeModulesPlugin],\r\n ...(this.options as any),\r\n });\r\n } catch (e) {\r\n console.log(e);\r\n throw e;\r\n }\r\n }\r\n\r\n /**\r\n * Build an alias map from tsconfig `paths` so esbuild resolves local\r\n * source aliases (e.g. `@warlock.js/cascade`) to their on-disk source\r\n * folders during bundling. Without this, `packages: \"external\"` would\r\n * leave those bare specifiers as raw imports and Node would fail to\r\n * resolve them at runtime (they aren't installed in node_modules).\r\n *\r\n * Only exact (non-wildcard) aliases are included — esbuild's `alias`\r\n * option doesn't support glob-style mappings.\r\n */\r\n private buildAliasMapFromTsconfig(): Record<string, string> {\r\n tsconfigManager.init();\r\n\r\n const alias: Record<string, string> = {};\r\n const baseUrl = path.resolve(process.cwd(), tsconfigManager.baseUrl);\r\n\r\n for (const [from, to] of Object.entries(tsconfigManager.aliases)) {\r\n if (from.endsWith(\"/*\") || !Array.isArray(to) || to.length === 0) {\r\n continue;\r\n }\r\n\r\n alias[from] = path.resolve(baseUrl, to[0]);\r\n }\r\n\r\n return alias;\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,IAAa,oBAAb,MAA+B;;uBAEI,YAAY,YAAY;wBAwChC;GACvB,SAAS;GACT,QAAQ;GACR,MAAM;GACN,QAAQ;EACV;;;;;CAxCA,MAAa,QAAuB;EAClC,QAAQ,IAAI,OAAO,KAAK,8BAA8B,CAAC;EAGvD,MAAM,KAAK,kBAAkB;EAG7B,MAAM,KAAK,sBAAsB;EAGjC,MAAM,KAAK,mBAAmB;EAG9B,MAAM,KAAK,OAAO;EAGlB,MAAM,qBAAqB,KAAK,aAAa;EAE7C,QAAQ,IAAI,OAAO,MAAM,iBAAiB,CAAC;EAC3C,QAAQ,IAAI,sCAAsC,OAAO,KAAK,eAAe,GAAG;CAClF;;;;CAKA,MAAc,oBAAmC;EAC/C,KAAK,UAAU,mBAAmB;EAGlC,MAAM,qBAAqB,KAAK,aAAa;CAC/C;;;;CAeA,MAAc,wBAAuC;EACnD,QAAQ,IAAI,OAAO,OAAO,mCAAmC,CAAC;EAG9D,MAAM,KAAK,kBAAkB;EAG7B,MAAM,KAAK,qBAAqB;EAGhC,MAAM,CAAC,SAAS,QAAQ,MAAM,UAAU,MAAM,QAAQ,IAAI;GACxD,KAAK,gBAAgB;GACrB,KAAK,eAAe;GACpB,KAAK,aAAa;GAClB,KAAK,eAAe;EACtB,CAAC;EAED,KAAK,iBAAiB;GAAE;GAAS;GAAQ;GAAM;EAAO;CACxD;;;;CAKA,MAAc,oBAAmC;EAC/C,IAAI,UAAU;;;;;;;;;EASd,IAAI,MAAM,gBAAgB,QAAQ,cAAc,CAAC,GAC/C,WAAW;EAGb,MAAM,aAAa,KAAK,KAAK,KAAK,eAAe,cAAc,GAAG,OAAO;CAC3E;;;;;CAMA,MAAc,WAAW,UAAqC;EAW5D,QAAO,MAPa,KAAK,MAHH,SAAS,YAGG;GAChC,KAHmB,QAGH;GAChB,UAAU;EACZ,CAAC,EAIW,CAAC,KAAK,SAAS,mBAAmB,KAAK,QAAQ,eAAe,EAAE,CAAC;CAC/E;;;;;CAMA,MAAc,oBAAoB,WAAsC;EAStE,QAAO,MALa,KAAK,MAHH,UAAU,cAGE;GAChC,KAHmB,QAGH;GAChB,UAAU;EACZ,CAAC,EAEW,CAAC,KAAK,SAAS,mBAAmB,KAAK,QAAQ,eAAe,EAAE,CAAC;CAC/E;;;;CAKA,MAAc,uBAAsC;EAQlD,MAAM,eAAc,MALA,KAAK,cAAc;GACrC,KAHsB,KAAK,KAAK,QAAQ,IAAI,GAAG,YAG5B;GACnB,UAAU;EACZ,CAAC,EAEwB,CAAC,KAAK,MAAM,EAAE,QAAQ,eAAe,EAAE,CAAC;EAEjE,MAAM,UAAoB,CACxB,0CACA,6DACF;EACA,MAAM,gBAA0B,CAAC;EACjC,MAAM,iBAA2B,CAAC;EAClC,MAAM,YAAsB,CAAC;EAE7B,KAAK,MAAM,cAAc,aAAa;GACpC,MAAM,mBAAmB,YAAY,UAAU;GAC/C,MAAM,UAAU,GAAG,iBAAiB;GACpC,cAAc,KAAK,UAAU,QAAQ,0BAA0B,WAAW,GAAG;GAC7E,eAAe,KAAK,eAAe,iBAAiB,KAAK,QAAQ,GAAG;GACpE,UAAU,KAAK,wCAAwC,iBAAiB,KAAK,QAAQ,GAAG;EAC1F;EAEA,IAAI,UAAU;GACZ,GAAG;GACH;GACA;GACA,GAAG;GACH;GACA;GACA,GAAG;GACH;GACA;GACA,GAAG;GACH;EACF,CAAC,CAAC,KAAK,IAAI;EAEX,IAAI,MAAM,gBAAgB,QAAQ,aAAa,CAAC,GAC9C,WAAW;EAGb,MAAM,aAAa,KAAK,KAAK,KAAK,eAAe,kBAAkB,GAAG,OAAO;CAC/E;;;;;CAMA,MAAc,kBAAoC;EAChD,MAAM,QAAQ,MAAM,KAAK,WAAW,eAAe;EACnD,IAAI,MAAM,WAAW,GAAG,OAAO;EAC/B,MAAM,KAAK,oBAAoB,OAAO,YAAY;EAClD,OAAO;CACT;;;;;CAMA,MAAc,iBAAmC;EAC/C,MAAM,QAAQ,MAAM,KAAK,oBAAoB,QAAQ;EACrD,IAAI,MAAM,WAAW,GAAG,OAAO;EAC/B,MAAM,KAAK,oBAAoB,OAAO,WAAW;EACjD,OAAO;CACT;;;;;CAMA,MAAc,eAAiC;EAC7C,MAAM,QAAQ,MAAM,KAAK,WAAW,MAAM;EAC1C,IAAI,MAAM,WAAW,GAAG,OAAO;EAC/B,MAAM,KAAK,oBAAoB,OAAO,SAAS;EAC/C,OAAO;CACT;;;;;CAMA,MAAc,iBAAmC;EAC/C,MAAM,QAAQ,MAAM,KAAK,WAAW,QAAQ;EAC5C,IAAI,MAAM,WAAW,GAAG,OAAO;EAC/B,MAAM,KAAK,oBAAoB,OAAO,WAAW;EACjD,OAAO;CACT;;;;CAKA,MAAc,oBAAoB,aAAuB,YAAmC;EAE1F,MAAM,UADU,YAAY,KAAK,eAAe,WAAW,WAAW,GAChD,CAAC,CAAC,KAAK,IAAI,IAAI;EACrC,MAAM,aAAa,KAAK,KAAK,KAAK,eAAe,UAAU,GAAG,OAAO;CACvE;;;;CAKA,MAAc,qBAAoC;EAChD,QAAQ,IAAI,OAAO,OAAO,8BAA8B,CAAC;EAGzD,MAAM,UAAoB;GACxB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAOA,QAAQ,KAAK,IAAI,qDAAqD;EAEtE,IAAI,KAAK,eAAe,QACtB,QAAQ,KAAK,6BAA2B;EAE1C,IAAI,KAAK,eAAe,SACtB,QAAQ,KAAK,8BAA4B;EAE3C,IAAI,KAAK,eAAe,MACtB,QAAQ,KAAK,2BAAyB;EAExC,IAAI,KAAK,eAAe,QACtB,QAAQ,KAAK,6BAA2B;EAI1C,QAAQ,KACN,IACA,mEACA,gEACA,qEACA,IACA,gFACA,mHACA,4CACF;EAEA,MAAM,UAAU,QAAQ,KAAK,IAAI,IAAI;EACrC,MAAM,aAAa,KAAK,KAAK,KAAK,eAAe,QAAQ,GAAG,OAAO;CACrE;;;;CAKA,MAAc,SAAwB;EACpC,QAAQ,IAAI,OAAO,QAAQ,6BAA6B,CAAC;EAEzD,MAAM,aAAa,KAAK,KAAK,KAAK,eAAe,QAAQ;EACzD,MAAM,SAAS,KAAK,QAAQ;EAC5B,MAAM,cAAc,KAAK,QAAQ;EAEjC,MAAM,YAAY,KAAK,SAAS,aAAa,KAAK,QAAQ,WAAW,CAAC;EAEtE,OAAO,KAAK,QAAQ;EACpB,OAAO,KAAK,QAAQ;EAEpB,MAAM,qBAAqB,MAAM;EAEjC,MAAM,QAAQ,KAAK,0BAA0B;EAG7C,IAAI;GAEJ,MAAM,QAAQ,MAAM;IAClB,UAAU;IACV,aAAa,CAAC,UAAU;IACxB,QAAQ;IAKR,WAAW;IACX,UAAU;IACV,QAAQ,KAAK,QAAS;IACtB,WAAW,KAAK,QAAS,cAAc,OAAO,WAAW,KAAK,QAAS;IACvE,QAAQ;IAIR,QAAQ,CAAC,QAAQ;IACjB,QAAQ;IACR,YAAY;IACZ;IACA,SAAS,CAAC,uBAAuB;IACjC,GAAI,KAAK;GACX,CAAC;EACD,SAAS,GAAG;GACV,QAAQ,IAAI,CAAC;GACb,MAAM;EACR;CACF;;;;;;;;;;;CAYA,AAAQ,4BAAoD;EAC1D,gBAAgB,KAAK;EAErB,MAAM,QAAgC,CAAC;EACvC,MAAM,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,gBAAgB,OAAO;EAEnE,KAAK,MAAM,CAAC,MAAM,OAAO,OAAO,QAAQ,gBAAgB,OAAO,GAAG;GAChE,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,QAAQ,EAAE,KAAK,GAAG,WAAW,GAC7D;GAGF,MAAM,QAAQ,KAAK,QAAQ,SAAS,GAAG,EAAE;EAC3C;EAEA,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"production-builder.mjs","names":[],"sources":["../../../../../../../core/src/production/production-builder.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n getFileAsync,\r\n getJsonFileAsync,\r\n putFileAsync,\r\n removeDirectoryAsync,\r\n} from \"@warlock.js/fs\";\r\nimport esbuild from \"esbuild\";\r\nimport glob from \"fast-glob\";\r\nimport path from \"path\";\r\nimport { tsconfigManager } from \"../dev-server/tsconfig-manager\";\r\nimport { appPath, rootPath, warlockPath } from \"../utils\";\r\nimport { assertGeneratedImportsAreDeclared } from \"./assert-generated-imports\";\r\nimport { nativeNodeModulesPlugin } from \"./esbuild-plugins\";\r\nimport { resolveBuildConfig, type ResolvedBuildConfig } from \"./resolve-build-config\";\r\nimport { toCamelCase, toKebabCase } from \"@mongez/reinforcements\";\r\n\r\n/**\r\n * Production Builder\r\n * Generates production-ready files and bundles them for deployment\r\n * Build options are loaded from warlock.config.ts\r\n */\r\nexport class ProductionBuilder {\r\n private options!: ResolvedBuildConfig;\r\n private readonly productionDir = warlockPath(\"production\");\r\n\r\n /**\r\n * Main build entry point\r\n */\r\n public async build(): Promise<void> {\r\n console.log(colors.cyan(\"Building for production...\\n\"));\r\n\r\n // Step 1: Initialize options from config\r\n await this.initializeOptions();\r\n\r\n // Step 2: Generate combined files\r\n await this.generateCombinedFiles();\r\n\r\n // Step 3: Generate entry point\r\n await this.generateEntryPoint();\r\n\r\n // Step 4: Refuse to bundle code the app could not resolve at runtime\r\n await this.assertGeneratedImports();\r\n\r\n // Step 5: Bundle with esbuild\r\n await this.bundle();\r\n\r\n // Step 6: Remove production folder\r\n await removeDirectoryAsync(this.productionDir);\r\n\r\n console.log(colors.green(\"Build complete!\"));\r\n console.log(`Start production server by running ${colors.cyan(\"warlock start\")}`);\r\n }\r\n\r\n /**\r\n * Initialize options from warlock.config.ts\r\n */\r\n private async initializeOptions(): Promise<void> {\r\n this.options = resolveBuildConfig();\r\n\r\n // Ensure production directory exists\r\n await ensureDirectoryAsync(this.productionDir);\r\n }\r\n\r\n /**\r\n * Track which special files were generated\r\n */\r\n private generatedFiles = {\r\n locales: false,\r\n events: false,\r\n main: false,\r\n routes: false,\r\n };\r\n\r\n /**\r\n * Generate all combined files\r\n */\r\n private async generateCombinedFiles(): Promise<void> {\r\n console.log(colors.yellow(\" Generating production files...\"));\r\n\r\n // Generate bootstrap.ts file\r\n await this.generateBootstrap();\r\n\r\n // Generate config loader\r\n await this.generateConfigLoader();\r\n\r\n // Generate special files and track which ones have content\r\n const [locales, events, main, routes] = await Promise.all([\r\n this.generateLocales(),\r\n this.generateEvents(),\r\n this.generateMain(),\r\n this.generateRoutes(),\r\n ]);\r\n\r\n this.generatedFiles = { locales, events, main, routes };\r\n }\r\n\r\n /**\r\n * Generate bootstrap.ts - ensures bootstrap() runs first and sets production environment\r\n */\r\n private async generateBootstrap(): Promise<void> {\r\n let content = `import { bootstrap, Application } from \"@warlock.js/core\";\r\n\r\n// Set production environment\r\nApplication.setRuntimeStrategy(\"production\");\r\nApplication.setEnvironment(\"production\");\r\n\r\n// Bootstrap the application\r\nbootstrap();\r\n`;\r\n if (await fileExistsAsync(appPath(\"bootstrap.ts\"))) {\r\n content += \"import './../../src/app/bootstrap';\\n\";\r\n }\r\n\r\n await putFileAsync(path.join(this.productionDir, \"bootstrap.ts\"), content);\r\n }\r\n\r\n /**\r\n * Fail the build when generated code imports a package the app does not\r\n * declare.\r\n *\r\n * The bundler is configured `packages: \"external\"`, so every bare specifier\r\n * written above survives verbatim into an artifact that resolves against the\r\n * **consuming app's** node_modules. A framework-internal dependency imported\r\n * there resolves by accident under npm/yarn hoisting and dies under pnpm —\r\n * which is how a production app shipped that could not boot. Rewriting the\r\n * offending import fixes today's bundle; this check is what stops the next\r\n * change to the generator from quietly reintroducing it.\r\n */\r\n private async assertGeneratedImports(): Promise<void> {\r\n const generatedPaths = await glob(\"**/*.{ts,tsx,js,mjs}\", {\r\n cwd: this.productionDir,\r\n absolute: false,\r\n });\r\n\r\n const [files, appPackage] = await Promise.all([\r\n Promise.all(\r\n generatedPaths.map(async (file) => ({\r\n file,\r\n content: await getFileAsync(path.join(this.productionDir, file)),\r\n })),\r\n ),\r\n getJsonFileAsync<{ dependencies?: Record<string, string> }>(rootPath(\"package.json\")),\r\n ]);\r\n\r\n assertGeneratedImportsAreDeclared(files, Object.keys(appPackage.dependencies ?? {}));\r\n }\r\n\r\n /**\r\n * Glob for module files matching a pattern\r\n * Returns relative paths from .warlock/production/ to src/app/\r\n */\r\n private async globModule(fileName: string): Promise<string[]> {\r\n const pattern = `**/${fileName}.{ts,tsx}`;\r\n const appDirectory = appPath();\r\n\r\n const files = await glob(pattern, {\r\n cwd: appDirectory,\r\n absolute: false,\r\n });\r\n\r\n // Convert to relative paths from .warlock/production/ to src/app/\r\n // e.g., \"users/main\" -> \"../../src/app/users/main\"\r\n return files.map((file) => \"../../src/app/\" + file.replace(/\\.(ts|tsx)$/, \"\"));\r\n }\r\n\r\n /**\r\n * Glob for files in a specific directory pattern\r\n * Returns relative paths from .warlock/production/ to src/app/\r\n */\r\n private async globModuleDirectory(directory: string): Promise<string[]> {\r\n const pattern = `**/${directory}/*.{ts,tsx}`;\r\n const appDirectory = appPath();\r\n\r\n const files = await glob(pattern, {\r\n cwd: appDirectory,\r\n absolute: false,\r\n });\r\n\r\n return files.map((file) => \"../../src/app/\" + file.replace(/\\.(ts|tsx)$/, \"\"));\r\n }\r\n\r\n /**\r\n * Generate config-loader.ts\r\n */\r\n private async generateConfigLoader(): Promise<void> {\r\n const configDirectory = path.join(process.cwd(), \"src/config\");\r\n\r\n const files = await glob(\"*.{ts,tsx}\", {\r\n cwd: configDirectory,\r\n absolute: false,\r\n });\r\n\r\n const configNames = files.map((f) => f.replace(/\\.(ts|tsx)$/, \"\"));\r\n\r\n // Only `@warlock.js/core` — the one package the app itself declares. See\r\n // `assertGeneratedImportsAreDeclared`: `@mongez/config` is core's own\r\n // dependency, so a bare import of it resolved by luck under npm/yarn\r\n // hoisting and died under pnpm's strict layout.\r\n const imports: string[] = [\r\n 'import { setConfig, configSpecialHandlers } from \"@warlock.js/core\";',\r\n ];\r\n const configImports: string[] = [];\r\n const configSetCalls: string[] = [];\r\n const executors: string[] = [];\r\n\r\n for (const configName of configNames) {\r\n const properConfigName = toCamelCase(configName);\r\n const varName = `${properConfigName}Config`;\r\n configImports.push(`import ${varName} from \"../../src/config/${configName}\";`);\r\n configSetCalls.push(`setConfig(\"${properConfigName}\", ${varName});`);\r\n executors.push(`await configSpecialHandlers.execute(\"${properConfigName}\", ${varName});`);\r\n }\r\n\r\n let content = [\r\n ...imports,\r\n \"\",\r\n \"// Config imports\",\r\n ...configImports,\r\n \"\",\r\n \"// Register configs\",\r\n ...configSetCalls,\r\n \"\",\r\n \"// Special handlers\",\r\n ...executors,\r\n \"\",\r\n ].join(\"\\n\");\r\n\r\n if (await fileExistsAsync(appPath(\"prestart.ts\"))) {\r\n content += \"import './../../src/app/prestart';\\n\";\r\n }\r\n\r\n await putFileAsync(path.join(this.productionDir, \"config-loader.ts\"), content);\r\n }\r\n\r\n /**\r\n * Generate locales.ts (only if there are locale files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateLocales(): Promise<boolean> {\r\n const files = await this.globModule(\"utils/locales\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"locales.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate events.ts (only if there are event files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateEvents(): Promise<boolean> {\r\n const files = await this.globModuleDirectory(\"events\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"events.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate main.ts (only if there are main files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateMain(): Promise<boolean> {\r\n const files = await this.globModule(\"main\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"main.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate routes.ts (only if there are route files)\r\n * @returns true if file was generated with content\r\n */\r\n private async generateRoutes(): Promise<boolean> {\r\n const files = await this.globModule(\"routes\");\r\n if (files.length === 0) return false;\r\n await this.generateImportsFile(files, \"routes.ts\");\r\n return true;\r\n }\r\n\r\n /**\r\n * Generate a file with imports from all given files\r\n */\r\n private async generateImportsFile(importPaths: string[], outputFile: string): Promise<void> {\r\n const imports = importPaths.map((importPath) => `import \"${importPath}\";`);\r\n const content = imports.join(\"\\n\") + \"\\n\";\r\n await putFileAsync(path.join(this.productionDir, outputFile), content);\r\n }\r\n\r\n /**\r\n * Generate the main entry point (app.ts)\r\n */\r\n private async generateEntryPoint(): Promise<void> {\r\n console.log(colors.yellow(\" Generating entry point...\"));\r\n\r\n // Build imports based on which files were generated\r\n const imports: string[] = [\r\n \"// 1. Bootstrap (loads .env, initializes framework)\",\r\n 'import \"./bootstrap\";',\r\n \"\",\r\n \"// 2. Load configs\",\r\n 'import \"./config-loader\";',\r\n \"\",\r\n \"// 3. Start early-phase connectors (database, cache, logger, ...)\",\r\n \"// so data sources, cache, etc. are ready before app code runs\",\r\n 'import { Application, connectorsManager, ConnectorLifecyclePhase } from \"@warlock.js/core\";',\r\n \"await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\",\r\n ];\r\n\r\n // App code uses dynamic `await import(...)` so each module's side\r\n // effects fire at THIS point in execution. Static imports would be\r\n // hoisted to module-instantiation time (before the early-phase await\r\n // resolves), which is exactly the bug this split is meant to fix.\r\n // Requires `splitting: true` in esbuild (set below in bundle()).\r\n imports.push(\"\", \"// 4. Load app code (events, locales, main, routes)\");\r\n\r\n if (this.generatedFiles.events) {\r\n imports.push('await import(\"./events\");');\r\n }\r\n if (this.generatedFiles.locales) {\r\n imports.push('await import(\"./locales\");');\r\n }\r\n if (this.generatedFiles.main) {\r\n imports.push('await import(\"./main\");');\r\n }\r\n if (this.generatedFiles.routes) {\r\n imports.push('await import(\"./routes\");');\r\n }\r\n\r\n // Start late-phase connectors after app code registers routes/listeners\r\n imports.push(\r\n \"\",\r\n \"// 5. Start late-phase connectors (http, socket) — routes and\",\r\n \"// listeners registered by app code are now ready to bind\",\r\n \"await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\",\r\n \"\",\r\n \"// 6. Signal a complete boot so `Application.onceBooted(...)` listeners fire\",\r\n \"Application.markBooted({ environment: Application.environment, runtimeStrategy: Application.runtimeStrategy });\",\r\n \"connectorsManager.shutdownOnProcessKill();\",\r\n );\r\n\r\n const content = imports.join(\"\\n\") + \"\\n\";\r\n await putFileAsync(path.join(this.productionDir, \"app.ts\"), content);\r\n }\r\n\r\n /**\r\n * Bundle with esbuild\r\n */\r\n private async bundle(): Promise<void> {\r\n console.log(colors.magenta(\" Bundling with esbuild...\"));\r\n\r\n const entryPoint = path.join(this.productionDir, \"app.ts\");\r\n const outDir = this.options.outdir!;\r\n const outFileName = this.options.outFile!;\r\n // Strip extension so entryNames produces \"<base>.js\" via esbuild\r\n const entryName = path.basename(outFileName, path.extname(outFileName));\r\n\r\n delete this.options.outFile;\r\n delete this.options.entryPath;\r\n\r\n await ensureDirectoryAsync(outDir);\r\n\r\n const alias = this.buildAliasMapFromTsconfig();\r\n\r\n\r\n try {\r\n \r\n await esbuild.build({\r\n platform: \"node\",\r\n entryPoints: [entryPoint],\r\n bundle: true,\r\n // Required so dynamic `await import(\"./main\")` in the generated\r\n // app.ts produces separate chunks loaded at the runtime call site,\r\n // instead of inlining the modules at instantiation time (which\r\n // would defeat the early/late phase split).\r\n splitting: true,\r\n packages: \"external\",\r\n minify: this.options!.minify,\r\n sourcemap: this.options!.sourcemap === true ? \"linked\" : this.options!.sourcemap,\r\n format: \"esm\",\r\n // Targeting a concrete Node version (not \"esnext\") so esbuild\r\n // transpiles TC39 stage 3 decorators into helpers — Node does not\r\n // implement them natively yet.\r\n target: [\"node22\"],\r\n outdir: outDir,\r\n entryNames: entryName,\r\n alias,\r\n plugins: [nativeNodeModulesPlugin],\r\n ...(this.options as any),\r\n });\r\n } catch (e) {\r\n console.log(e);\r\n throw e;\r\n }\r\n }\r\n\r\n /**\r\n * Build an alias map from tsconfig `paths` so esbuild resolves local\r\n * source aliases (e.g. `@warlock.js/cascade`) to their on-disk source\r\n * folders during bundling. Without this, `packages: \"external\"` would\r\n * leave those bare specifiers as raw imports and Node would fail to\r\n * resolve them at runtime (they aren't installed in node_modules).\r\n *\r\n * Only exact (non-wildcard) aliases are included — esbuild's `alias`\r\n * option doesn't support glob-style mappings.\r\n */\r\n private buildAliasMapFromTsconfig(): Record<string, string> {\r\n tsconfigManager.init();\r\n\r\n const alias: Record<string, string> = {};\r\n const baseUrl = path.resolve(process.cwd(), tsconfigManager.baseUrl);\r\n\r\n for (const [from, to] of Object.entries(tsconfigManager.aliases)) {\r\n if (from.endsWith(\"/*\") || !Array.isArray(to) || to.length === 0) {\r\n continue;\r\n }\r\n\r\n alias[from] = path.resolve(baseUrl, to[0]);\r\n }\r\n\r\n return alias;\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { BOOT_SIGNAL_ENV_KEY, isBootSignal } from "../application/boot-signal.mjs";
|
|
2
|
+
import { displayMissingReadinessNotice, displayProductionReadyBanner, displayProductionStartFailure } from "../cli/cli-commands.utils.mjs";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
|
|
5
|
+
//#region ../core/src/production/production-supervisor.ts
|
|
6
|
+
/**
|
|
7
|
+
* How long a still-running child may stay silent before we say so.
|
|
8
|
+
*
|
|
9
|
+
* Only reached by a bundle built before readiness reporting existed; a current
|
|
10
|
+
* bundle signals as soon as its late-phase connectors are up.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_READINESS_NOTICE_DELAY_MS = 1e4;
|
|
13
|
+
/**
|
|
14
|
+
* Run the production bundle in a child process and report honestly on it.
|
|
15
|
+
*
|
|
16
|
+
* The supervisor exists because a parent with `stdio: "inherit"` cannot see
|
|
17
|
+
* whether its child ever booted — it only sees the process exit. It therefore
|
|
18
|
+
* opens an IPC channel and treats the child's `warlock:ready` signal, emitted
|
|
19
|
+
* by `Application.markBooted`, as the single source of truth for "started":
|
|
20
|
+
*
|
|
21
|
+
* 1. The success banner is printed only on that signal.
|
|
22
|
+
* 2. A child that dies before signalling is reported as a failed start and
|
|
23
|
+
* forced to a non-zero exit, even if it exited 0.
|
|
24
|
+
* 3. A child still running without having signalled draws a note on stderr —
|
|
25
|
+
* never stdout, which would be the false-green this whole channel prevents.
|
|
26
|
+
*
|
|
27
|
+
* Resolves rather than exiting so the outcome stays assertable in a test; the
|
|
28
|
+
* `start` command turns the result into the process exit code.
|
|
29
|
+
*/
|
|
30
|
+
function superviseProductionProcess({ nodeArgs, cwd = process.cwd(), env = process.env, readinessNoticeDelayMs = DEFAULT_READINESS_NOTICE_DELAY_MS }) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const child = spawn(process.execPath, nodeArgs, {
|
|
33
|
+
stdio: [
|
|
34
|
+
"inherit",
|
|
35
|
+
"inherit",
|
|
36
|
+
"inherit",
|
|
37
|
+
"ipc"
|
|
38
|
+
],
|
|
39
|
+
cwd,
|
|
40
|
+
env: {
|
|
41
|
+
...env,
|
|
42
|
+
[BOOT_SIGNAL_ENV_KEY]: "1"
|
|
43
|
+
},
|
|
44
|
+
detached: false
|
|
45
|
+
});
|
|
46
|
+
const startedAt = Date.now();
|
|
47
|
+
let isReady = false;
|
|
48
|
+
let isShuttingDown = false;
|
|
49
|
+
let isSettled = false;
|
|
50
|
+
const readinessNoticeTimer = setTimeout(() => {
|
|
51
|
+
if (isReady) return;
|
|
52
|
+
displayMissingReadinessNotice(Date.now() - startedAt);
|
|
53
|
+
}, readinessNoticeDelayMs);
|
|
54
|
+
readinessNoticeTimer.unref?.();
|
|
55
|
+
const settle = (result) => {
|
|
56
|
+
if (isSettled) return;
|
|
57
|
+
isSettled = true;
|
|
58
|
+
clearTimeout(readinessNoticeTimer);
|
|
59
|
+
process.off("SIGTERM", onSigterm);
|
|
60
|
+
process.off("SIGINT", onSigint);
|
|
61
|
+
resolve(result);
|
|
62
|
+
};
|
|
63
|
+
child.on("message", (message) => {
|
|
64
|
+
if (isReady || !isBootSignal(message)) return;
|
|
65
|
+
isReady = true;
|
|
66
|
+
clearTimeout(readinessNoticeTimer);
|
|
67
|
+
displayProductionReadyBanner({ bootDurationMs: message.bootDurationMs }).catch((error) => {
|
|
68
|
+
console.error(error);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
child.on("error", (error) => {
|
|
72
|
+
displayProductionStartFailure(1);
|
|
73
|
+
console.error(error);
|
|
74
|
+
settle({
|
|
75
|
+
exitCode: 1,
|
|
76
|
+
ready: false
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
const onSigterm = () => {
|
|
80
|
+
if (isShuttingDown) return;
|
|
81
|
+
isShuttingDown = true;
|
|
82
|
+
child.kill("SIGTERM");
|
|
83
|
+
};
|
|
84
|
+
const onSigint = () => {
|
|
85
|
+
isShuttingDown = true;
|
|
86
|
+
};
|
|
87
|
+
process.on("SIGTERM", onSigterm);
|
|
88
|
+
process.on("SIGINT", onSigint);
|
|
89
|
+
child.on("exit", (code) => {
|
|
90
|
+
if (!isReady && !isShuttingDown) {
|
|
91
|
+
const exitCode = code === 0 || code === null ? 1 : code;
|
|
92
|
+
displayProductionStartFailure(exitCode);
|
|
93
|
+
return settle({
|
|
94
|
+
exitCode,
|
|
95
|
+
ready: false
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
settle({
|
|
99
|
+
exitCode: code ?? 0,
|
|
100
|
+
ready: isReady
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
export { superviseProductionProcess };
|
|
108
|
+
//# sourceMappingURL=production-supervisor.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"production-supervisor.mjs","names":[],"sources":["../../../../../../../core/src/production/production-supervisor.ts"],"sourcesContent":["import { spawn } from \"child_process\";\nimport { BOOT_SIGNAL_ENV_KEY, isBootSignal } from \"../application/boot-signal\";\nimport {\n displayMissingReadinessNotice,\n displayProductionReadyBanner,\n displayProductionStartFailure,\n} from \"../cli/cli-commands.utils\";\n\n/**\n * How long a still-running child may stay silent before we say so.\n *\n * Only reached by a bundle built before readiness reporting existed; a current\n * bundle signals as soon as its late-phase connectors are up.\n */\nexport const DEFAULT_READINESS_NOTICE_DELAY_MS = 10_000;\n\nexport type ProductionSupervisorOptions = {\n /** Arguments handed to the node binary, entry file included. */\n nodeArgs: string[];\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n readinessNoticeDelayMs?: number;\n};\n\nexport type ProductionSupervisorResult = {\n /** What the CLI should exit with — never 0 for a boot that never completed. */\n exitCode: number;\n /** Whether the child ever reported a finished boot. */\n ready: boolean;\n};\n\n/**\n * Run the production bundle in a child process and report honestly on it.\n *\n * The supervisor exists because a parent with `stdio: \"inherit\"` cannot see\n * whether its child ever booted — it only sees the process exit. It therefore\n * opens an IPC channel and treats the child's `warlock:ready` signal, emitted\n * by `Application.markBooted`, as the single source of truth for \"started\":\n *\n * 1. The success banner is printed only on that signal.\n * 2. A child that dies before signalling is reported as a failed start and\n * forced to a non-zero exit, even if it exited 0.\n * 3. A child still running without having signalled draws a note on stderr —\n * never stdout, which would be the false-green this whole channel prevents.\n *\n * Resolves rather than exiting so the outcome stays assertable in a test; the\n * `start` command turns the result into the process exit code.\n */\nexport function superviseProductionProcess({\n nodeArgs,\n cwd = process.cwd(),\n env = process.env,\n readinessNoticeDelayMs = DEFAULT_READINESS_NOTICE_DELAY_MS,\n}: ProductionSupervisorOptions): Promise<ProductionSupervisorResult> {\n return new Promise<ProductionSupervisorResult>((resolve) => {\n // On Windows, we need to be careful with signals - the console sends Ctrl+C\n // to all processes in the group, so we just need to not interfere.\n // `process.execPath`, not \"node\": the bare name needs `node` on PATH, which\n // a systemd unit, a cron job, or a slim container may not provide — and a\n // PATH `node` that *is* present can be a different version than the one\n // running this CLI. execPath is the binary already executing us.\n //\n // The fourth stdio slot opens the IPC channel the child reports readiness\n // on. Without it `process.send` is undefined in the child and we would be\n // back to guessing whether the boot succeeded.\n const child = spawn(process.execPath, nodeArgs, {\n stdio: [\"inherit\", \"inherit\", \"inherit\", \"ipc\"],\n cwd,\n // The flag identifies us as a parent that speaks the boot protocol. The\n // child stays silent without it, so an app running under pm2 or any other\n // supervisor never writes to a channel that belongs to someone else.\n env: { ...env, [BOOT_SIGNAL_ENV_KEY]: \"1\" },\n // Important: keep child in same process group for proper signal handling\n detached: false,\n });\n\n const startedAt = Date.now();\n let isReady = false;\n let isShuttingDown = false;\n let isSettled = false;\n\n const readinessNoticeTimer = setTimeout(() => {\n if (isReady) {\n return;\n }\n\n displayMissingReadinessNotice(Date.now() - startedAt);\n }, readinessNoticeDelayMs);\n\n // A diagnostic timer must never be the reason the CLI stays alive.\n readinessNoticeTimer.unref?.();\n\n const settle = (result: ProductionSupervisorResult) => {\n if (isSettled) {\n return;\n }\n\n isSettled = true;\n clearTimeout(readinessNoticeTimer);\n process.off(\"SIGTERM\", onSigterm);\n process.off(\"SIGINT\", onSigint);\n\n resolve(result);\n };\n\n child.on(\"message\", (message: unknown) => {\n if (isReady || !isBootSignal(message)) {\n return;\n }\n\n isReady = true;\n clearTimeout(readinessNoticeTimer);\n\n displayProductionReadyBanner({ bootDurationMs: message.bootDurationMs }).catch((error) => {\n // The server is up either way — a banner that fails to render must not\n // be reported as, or turn into, a boot failure.\n console.error(error);\n });\n });\n\n // A spawn that never starts (missing binary, EACCES) emits `error` and no\n // `exit`, so without this the command would hang silently on a failure.\n child.on(\"error\", (error) => {\n displayProductionStartFailure(1);\n console.error(error);\n\n settle({ exitCode: 1, ready: false });\n });\n\n // SIGTERM doesn't auto-propagate like SIGINT does on Windows.\n const onSigterm = () => {\n if (isShuttingDown) {\n return;\n }\n\n isShuttingDown = true;\n child.kill(\"SIGTERM\");\n };\n\n // On Windows Ctrl+C reaches both processes, so the child already has it —\n // we only record that this exit was asked for.\n const onSigint = () => {\n isShuttingDown = true;\n };\n\n process.on(\"SIGTERM\", onSigterm);\n process.on(\"SIGINT\", onSigint);\n\n child.on(\"exit\", (code) => {\n // A child that dies before reporting readiness never served a request.\n // Exiting 0 there — which a clean `process.exit(0)` in failing app code\n // would do — reports a boot failure as a successful run, so the exit code\n // is forced non-zero to match what actually happened.\n //\n // An operator who interrupts the boot is not a failed boot, so a signalled\n // shutdown keeps the child's own exit code.\n if (!isReady && !isShuttingDown) {\n const exitCode = code === 0 || code === null ? 1 : code;\n\n displayProductionStartFailure(exitCode);\n\n return settle({ exitCode, ready: false });\n }\n\n settle({ exitCode: code ?? 0, ready: isReady });\n });\n });\n}\n"],"mappings":";;;;;;;;;;;AAcA,MAAa,oCAAoC;;;;;;;;;;;;;;;;;;AAkCjD,SAAgB,2BAA2B,EACzC,UACA,MAAM,QAAQ,IAAI,GAClB,MAAM,QAAQ,KACd,yBAAyB,qCAC0C;CACnE,OAAO,IAAI,SAAqC,YAAY;EAW1D,MAAM,QAAQ,MAAM,QAAQ,UAAU,UAAU;GAC9C,OAAO;IAAC;IAAW;IAAW;IAAW;GAAK;GAC9C;GAIA,KAAK;IAAE,GAAG;KAAM,sBAAsB;GAAI;GAE1C,UAAU;EACZ,CAAC;EAED,MAAM,YAAY,KAAK,IAAI;EAC3B,IAAI,UAAU;EACd,IAAI,iBAAiB;EACrB,IAAI,YAAY;EAEhB,MAAM,uBAAuB,iBAAiB;GAC5C,IAAI,SACF;GAGF,8BAA8B,KAAK,IAAI,IAAI,SAAS;EACtD,GAAG,sBAAsB;EAGzB,qBAAqB,QAAQ;EAE7B,MAAM,UAAU,WAAuC;GACrD,IAAI,WACF;GAGF,YAAY;GACZ,aAAa,oBAAoB;GACjC,QAAQ,IAAI,WAAW,SAAS;GAChC,QAAQ,IAAI,UAAU,QAAQ;GAE9B,QAAQ,MAAM;EAChB;EAEA,MAAM,GAAG,YAAY,YAAqB;GACxC,IAAI,WAAW,CAAC,aAAa,OAAO,GAClC;GAGF,UAAU;GACV,aAAa,oBAAoB;GAEjC,6BAA6B,EAAE,gBAAgB,QAAQ,eAAe,CAAC,CAAC,CAAC,OAAO,UAAU;IAGxF,QAAQ,MAAM,KAAK;GACrB,CAAC;EACH,CAAC;EAID,MAAM,GAAG,UAAU,UAAU;GAC3B,8BAA8B,CAAC;GAC/B,QAAQ,MAAM,KAAK;GAEnB,OAAO;IAAE,UAAU;IAAG,OAAO;GAAM,CAAC;EACtC,CAAC;EAGD,MAAM,kBAAkB;GACtB,IAAI,gBACF;GAGF,iBAAiB;GACjB,MAAM,KAAK,SAAS;EACtB;EAIA,MAAM,iBAAiB;GACrB,iBAAiB;EACnB;EAEA,QAAQ,GAAG,WAAW,SAAS;EAC/B,QAAQ,GAAG,UAAU,QAAQ;EAE7B,MAAM,GAAG,SAAS,SAAS;GAQzB,IAAI,CAAC,WAAW,CAAC,gBAAgB;IAC/B,MAAM,WAAW,SAAS,KAAK,SAAS,OAAO,IAAI;IAEnD,8BAA8B,QAAQ;IAEtC,OAAO,OAAO;KAAE;KAAU,OAAO;IAAM,CAAC;GAC1C;GAEA,OAAO;IAAE,UAAU,QAAQ;IAAG,OAAO;GAAQ,CAAC;EAChD,CAAC;CACH,CAAC;AACH"}
|
|
@@ -97,6 +97,22 @@ declare class Storage extends ScopedStorage implements StorageManagerContract {
|
|
|
97
97
|
* @internal
|
|
98
98
|
*/
|
|
99
99
|
protected loadDriversFromConfig(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Register the built-in `local` driver, rooted at `uploadsPath()`.
|
|
102
|
+
*
|
|
103
|
+
* The storage connector starts unconditionally — unlike database, cache and
|
|
104
|
+
* herald, it does not early-return when `config.get("storage")` is absent —
|
|
105
|
+
* on the stated grounds that `init()` falls back to a built-in local driver so
|
|
106
|
+
* file storage works out of the box. That fallback did not exist: `init()`
|
|
107
|
+
* resolved the default driver *name* and then found no config registered
|
|
108
|
+
* under it, so **any app without `src/config/storage.ts` failed to boot**
|
|
109
|
+
* with `Storage driver "local" is not configured`. Only scaffolded apps, which
|
|
110
|
+
* always ship that file, hid it.
|
|
111
|
+
*
|
|
112
|
+
* Registered before the configured drivers so an app that defines its own
|
|
113
|
+
* `local` entry overrides this one rather than fighting it.
|
|
114
|
+
*/
|
|
115
|
+
protected registerBuiltInLocalDriver(): void;
|
|
100
116
|
/**
|
|
101
117
|
* Get a scoped storage for a specific driver
|
|
102
118
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.mts","names":[],"sources":["../../../../../../../core/src/storage/storage.ts"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"storage.d.mts","names":[],"sources":["../../../../../../../core/src/storage/storage.ts"],"mappings":"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"}
|
package/esm/storage/storage.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { uploadsPath } from "../utils/paths.mjs";
|
|
2
|
+
import { storageConfig, storageConfigurations } from "./config.mjs";
|
|
2
3
|
import { StorageFile } from "./storage-file.mjs";
|
|
3
4
|
import { safeFetchToBuffer } from "./utils/safe-fetch.mjs";
|
|
4
5
|
import { ScopedStorage } from "./scoped-storage.mjs";
|
|
@@ -105,10 +106,32 @@ var Storage = class extends ScopedStorage {
|
|
|
105
106
|
* @internal
|
|
106
107
|
*/
|
|
107
108
|
loadDriversFromConfig() {
|
|
109
|
+
this.registerBuiltInLocalDriver();
|
|
108
110
|
const drivers = storageConfig("drivers", {});
|
|
109
111
|
for (const [name, config] of Object.entries(drivers)) this.configs.set(name, config);
|
|
110
112
|
}
|
|
111
113
|
/**
|
|
114
|
+
* Register the built-in `local` driver, rooted at `uploadsPath()`.
|
|
115
|
+
*
|
|
116
|
+
* The storage connector starts unconditionally — unlike database, cache and
|
|
117
|
+
* herald, it does not early-return when `config.get("storage")` is absent —
|
|
118
|
+
* on the stated grounds that `init()` falls back to a built-in local driver so
|
|
119
|
+
* file storage works out of the box. That fallback did not exist: `init()`
|
|
120
|
+
* resolved the default driver *name* and then found no config registered
|
|
121
|
+
* under it, so **any app without `src/config/storage.ts` failed to boot**
|
|
122
|
+
* with `Storage driver "local" is not configured`. Only scaffolded apps, which
|
|
123
|
+
* always ship that file, hid it.
|
|
124
|
+
*
|
|
125
|
+
* Registered before the configured drivers so an app that defines its own
|
|
126
|
+
* `local` entry overrides this one rather than fighting it.
|
|
127
|
+
*/
|
|
128
|
+
registerBuiltInLocalDriver() {
|
|
129
|
+
this.configs.set("local", storageConfigurations.local({
|
|
130
|
+
root: uploadsPath(),
|
|
131
|
+
urlPrefix: "/uploads"
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
112
135
|
* Get a scoped storage for a specific driver
|
|
113
136
|
*
|
|
114
137
|
* Returns a `ScopedStorage` instance that wraps the specified driver.
|
|
@@ -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 { storageConfig } 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 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 * 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 await events.triggerAll(`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 } =\n 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 mimeType = mimeType || match[1];\n data = match[2];\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,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;;;;;;;;;CAUA,IAAoB,eAAsC;EAExD,MAAM,gBAAgB,qBAAqB,UAAU;EAErD,IAAI,eAAe,OAAO;EAE1B,OAAO,KAAK;CACd;;;;;CAUA,AAAU,wBAA8B;EACtC,MAAM,UAAU,cAAmD,WAAW,CAAC,CAAC;EAEhF,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,OAAO,GACjD,KAAK,QAAQ,IAAI,MAAM,MAAM;CAEjC;;;;;;;;;;;;;;;;;;;;;;;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;EACf,MAAM,OAAO,WAAW,WAAW,SAAS,OAAO;CACrD;;;;;;;;;;;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,eACjE,WAAW,CAAC;EAEd,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,WAAW,YAAY,MAAM;IAC7B,OAAO,MAAM;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"}
|
|
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 await events.triggerAll(`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 } =\n 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 mimeType = mimeType || match[1];\n data = match[2];\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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"}
|
package/esm/tests/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { isTestServerRunning, startHttpTestServer, stopHttpTestServer } from "./start-http-development-server.mjs";
|
|
1
|
+
import { StartHttpTestServerOptions, isTestServerRunning, startHttpTestServer, stopHttpTestServer } from "./start-http-development-server.mjs";
|
|
2
2
|
import { expectJson, getTestServerUrl, parseJsonResponse, testDelete, testGet, testPatch, testPost, testPut, testRequest } from "./test-helpers.mjs";
|
|
3
|
+
import { TEST_SERVER_PORT_ENV_KEY, publishTestServerPort, withdrawTestServerPort } from "./test-server-port-channel.mjs";
|
|
3
4
|
import { setupTest } from "./vitest-setup.mjs";
|
package/esm/tests/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TEST_SERVER_PORT_ENV_KEY, publishTestServerPort, withdrawTestServerPort } from "./test-server-port-channel.mjs";
|
|
1
2
|
import { isTestServerRunning, startHttpTestServer, stopHttpTestServer } from "./start-http-development-server.mjs";
|
|
2
3
|
import { expectJson, getTestServerUrl, parseJsonResponse, testDelete, testGet, testPatch, testPost, testPut, testRequest } from "./test-helpers.mjs";
|
|
3
4
|
import { setupTest } from "./vitest-setup.mjs";
|