{"_id":"@aginix/adonis-vulcan-storage","_rev":"4-a204351da8cbde3c13d91622bd3e3093","name":"@aginix/adonis-vulcan-storage","dist-tags":{"beta":"0.1.0-beta.0","latest":"0.2.1"},"versions":{"0.1.0-beta.0":{"name":"@aginix/adonis-vulcan-storage","version":"0.1.0-beta.0","keywords":["adonisjs","adonis","drive","storage","object-storage","s3","gcs","signed-url","lucid"],"author":{"name":"n3n"},"license":"UNLICENSED","_id":"@aginix/adonis-vulcan-storage@0.1.0-beta.0","maintainers":[{"name":"n3n","email":"nonpawit.tee@gmail.com"}],"c8":{"exclude":["tests/**"],"reporter":["text","html"]},"dist":{"shasum":"8daebc08d4e0ad548cb80c2512fb778985c44e96","tarball":"https://registry.npmjs.org/@aginix/adonis-vulcan-storage/-/adonis-vulcan-storage-0.1.0-beta.0.tgz","fileCount":47,"integrity":"sha512-x4EzD8bxa2m9wMWoLUpqcLnyC3YHpGP6usLRJ8EE2So4a+hb2IrbrHMuNxEofZPR9W7wVOtBem4o1dz8TI/Z9w==","signatures":[{"sig":"MEQCIHmtGKw4GEfLbkF4UHWTlBUoG5m6PhBdFEr0UV8EOLIJAiBJQSP8Cpovg5WwU1BUt7XX2SEB9df62xclE4SOo3pgJA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":61857},"main":"build/index.js","type":"module","types":"./build/index.d.ts","tsdown":{"dts":false,"clean":true,"entry":["./index.ts","./configure.ts","./src/types.ts","./src/define_config.ts","./src/errors.ts","./src/path_validator.ts","./src/security_rules.ts","./src/runtime_state.ts","./src/storage_object.ts","./src/storage_service.ts","./src/storage_objects_controller.ts","./src/validators.ts","./src/routes.ts","./providers/storage_provider.ts","./services/main.ts"],"format":"esm","minify":"dce-only","outDir":"./build","target":"esnext","external":["@adonisjs/core","@adonisjs/core/ace","@adonisjs/core/exceptions","@adonisjs/core/helpers/string","@adonisjs/core/http","@adonisjs/core/providers/vinejs_provider","@adonisjs/core/services/app","@adonisjs/core/services/router","@adonisjs/core/types","@adonisjs/core/commands/configure","@adonisjs/drive","@adonisjs/drive/services/main","@adonisjs/drive/types","@adonisjs/lucid","@adonisjs/lucid/orm","@adonisjs/lucid/types/model","@vinejs/vine","luxon"],"treeshake":false,"sourcemaps":false,"fixedExtension":false},"engines":{"node":">=24.0.0"},"exports":{".":"./build/index.js","./types":"./build/src/types.js","./errors":"./build/src/errors.js","./routes":"./build/src/routes.js","./define_config":"./build/src/define_config.js","./services/main":"./build/services/main.js","./path_validator":"./build/src/path_validator.js","./security_rules":"./build/src/security_rules.js","./storage_object":"./build/src/storage_object.js","./storage_service":"./build/src/storage_service.js","./storage_provider":"./build/providers/storage_provider.js"},"scripts":{"lint":"eslint .","test":"c8 npm run quick:test","build":"npm run compile","format":"prettier --write .","compile":"tsdown && tsc --emitDeclarationOnly --declaration","pretest":"npm run lint","release":"release-it","version":"npm run build","typecheck":"tsc --noEmit","precompile":"npm run lint","quick:test":"node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts","postcompile":"npm run copy:templates","copy:templates":"copyfiles --up=1 \"stubs/**/*.stub\" build","prepublishOnly":"npm run build"},"_npmUser":{"name":"n3n","email":"nonpawit.tee@gmail.com"},"prettier":"@adonisjs/prettier-config","release-it":{"git":{"push":true,"tagName":"adonis-vulcan-storage-v${version}","commitMessage":"chore(adonis-vulcan-storage): release ${version}","tagAnnotation":"adonis-vulcan-storage-v${version}","requireUpstream":true,"requireCleanWorkingDir":true},"npm":{"publish":true,"skipChecks":true},"github":{"release":true,"releaseName":"@aginix/adonis-vulcan-storage ${version}"},"plugins":{"@release-it/conventional-changelog":{"preset":{"name":"angular"}}}},"_npmVersion":"11.8.0","description":"Drop-in object-storage HTTP module for AdonisJS: signed upload/download URLs, Lucid-backed metadata, path-based security rules, on top of @adonisjs/drive.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^11.0.0","luxon":"^3.7.2","eslint":"^10.0.3","tsdown":"^0.21.0","prettier":"^3.8.1","copyfiles":"^2.4.1","cross-env":"^10.1.0","release-it":"^19.2.4","typescript":"^5.9.3","@types/node":"^25.3.5","@japa/assert":"^4.2.0","@japa/runner":"^5.3.0","@types/luxon":"^3.7.1","@vinejs/vine":"^4.1.0","@adonisjs/core":"^7.0.1","@adonisjs/drive":"^4.0.0","@adonisjs/lucid":"^22.0.0","@poppinss/ts-exec":"^1.4.4","@adonisjs/tsconfig":"^2.0.0","@adonisjs/assembler":"^8.0.0","@adonisjs/eslint-config":"^3.0.0","@adonisjs/prettier-config":"^1.4.5","@release-it/conventional-changelog":"^10.0.5"},"peerDependencies":{"luxon":"^3.0.0","@vinejs/vine":"^4.0.0","@adonisjs/core":"^7.0.0","@adonisjs/drive":"^4.0.0","@adonisjs/lucid":"^22.0.0","@adonisjs/assembler":"^8.0.0"},"peerDependenciesMeta":{"@adonisjs/assembler":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/adonis-vulcan-storage_0.1.0-beta.0_1778744562018_0.8296624310655814","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@aginix/adonis-vulcan-storage","version":"0.1.0","keywords":["adonisjs","adonis","drive","storage","object-storage","s3","gcs","signed-url","lucid"],"author":{"name":"n3n"},"license":"UNLICENSED","_id":"@aginix/adonis-vulcan-storage@0.1.0","maintainers":[{"name":"n3n","email":"nonpawit.tee@gmail.com"}],"c8":{"exclude":["tests/**"],"reporter":["text","html"]},"dist":{"shasum":"cd7f792c4d3a800d1983f30b768ec6e3b8447c6c","tarball":"https://registry.npmjs.org/@aginix/adonis-vulcan-storage/-/adonis-vulcan-storage-0.1.0.tgz","fileCount":48,"integrity":"sha512-zEExep4VzeN+dm7xT341m3sU+QvrGKvZl0q/iHmL4fOfg7ytgvoZfaMmBlUGinMxHmHydQkTSlS+MuoRKws3OQ==","signatures":[{"sig":"MEYCIQDoWvbYusO6V6udxFA5trNIvvD/kml/uEcfafCDOebMJQIhAJzQQmtsW4yhEumQ/qfAf6pvykV9qPI05ClOZJ7cZYyX","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":80992},"main":"build/index.js","type":"module","types":"./build/index.d.ts","tsdown":{"dts":false,"clean":true,"entry":["./index.ts","./configure.ts","./src/types.ts","./src/define_config.ts","./src/errors.ts","./src/path_validator.ts","./src/security_rules.ts","./src/runtime_state.ts","./src/storage_object.ts","./src/storage_service.ts","./src/storage_objects_controller.ts","./src/validators.ts","./src/routes.ts","./providers/storage_provider.ts","./services/main.ts"],"format":"esm","minify":"dce-only","outDir":"./build","target":"esnext","external":["@adonisjs/core","@adonisjs/core/ace","@adonisjs/core/exceptions","@adonisjs/core/helpers/string","@adonisjs/core/http","@adonisjs/core/providers/vinejs_provider","@adonisjs/core/services/app","@adonisjs/core/services/router","@adonisjs/core/types","@adonisjs/core/commands/configure","@adonisjs/drive","@adonisjs/drive/services/main","@adonisjs/drive/types","@adonisjs/lucid","@adonisjs/lucid/orm","@adonisjs/lucid/types/model","@vinejs/vine","luxon"],"treeshake":false,"sourcemaps":false,"fixedExtension":false},"engines":{"node":">=24.0.0"},"exports":{".":"./build/index.js","./types":"./build/src/types.js","./errors":"./build/src/errors.js","./routes":"./build/src/routes.js","./define_config":"./build/src/define_config.js","./services/main":"./build/services/main.js","./path_validator":"./build/src/path_validator.js","./security_rules":"./build/src/security_rules.js","./storage_object":"./build/src/storage_object.js","./storage_service":"./build/src/storage_service.js","./storage_provider":"./build/providers/storage_provider.js"},"scripts":{"lint":"eslint .","test":"c8 npm run quick:test","build":"npm run compile","format":"prettier --write .","compile":"tsdown && tsc --emitDeclarationOnly --declaration","pretest":"npm run lint","release":"release-it","version":"npm run build","typecheck":"tsc --noEmit","precompile":"npm run lint","quick:test":"node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts","postcompile":"npm run copy:templates","copy:templates":"copyfiles --up=1 \"stubs/**/*.stub\" build","prepublishOnly":"npm run build"},"_npmUser":{"name":"n3n","email":"nonpawit.tee@gmail.com"},"prettier":"@adonisjs/prettier-config","release-it":{"git":{"push":true,"tagName":"adonis-vulcan-storage-v${version}","commitMessage":"chore(adonis-vulcan-storage): release ${version}","tagAnnotation":"adonis-vulcan-storage-v${version}","requireUpstream":true,"requireCleanWorkingDir":true},"npm":{"publish":true,"skipChecks":true},"github":{"release":true,"releaseName":"@aginix/adonis-vulcan-storage ${version}"},"plugins":{"@release-it/conventional-changelog":{"preset":{"name":"angular"}}}},"_npmVersion":"11.8.0","description":"Drop-in object-storage HTTP module for AdonisJS: signed upload/download URLs, Lucid-backed metadata, path-based security rules, on top of @adonisjs/drive.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^11.0.0","luxon":"^3.7.2","eslint":"^10.0.3","tsdown":"^0.21.0","tempura":"^0.4.1","prettier":"^3.8.1","copyfiles":"^2.4.1","cross-env":"^10.1.0","release-it":"^19.2.4","typescript":"^5.9.3","@types/node":"^25.3.5","@japa/assert":"^4.2.0","@japa/runner":"^5.3.0","@types/luxon":"^3.7.1","@vinejs/vine":"^4.1.0","@adonisjs/core":"^7.0.1","@adonisjs/drive":"^4.0.0","@adonisjs/lucid":"^22.0.0","@poppinss/ts-exec":"^1.4.4","@adonisjs/tsconfig":"^2.0.0","@adonisjs/assembler":"^8.0.0","@adonisjs/eslint-config":"^3.0.0","@adonisjs/prettier-config":"^1.4.5","@release-it/conventional-changelog":"^10.0.5"},"peerDependencies":{"luxon":"^3.0.0","@vinejs/vine":"^4.0.0","@adonisjs/core":"^7.0.0","@adonisjs/drive":"^4.0.0","@adonisjs/lucid":"^22.0.0","@adonisjs/assembler":"^8.0.0"},"peerDependenciesMeta":{"@adonisjs/assembler":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/adonis-vulcan-storage_0.1.0_1778782907019_0.9829569369102207","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@aginix/adonis-vulcan-storage","version":"0.2.0","keywords":["adonisjs","adonis","drive","storage","object-storage","s3","gcs","signed-url","lucid"],"author":{"name":"n3n"},"license":"UNLICENSED","_id":"@aginix/adonis-vulcan-storage@0.2.0","maintainers":[{"name":"n3n","email":"nonpawit.tee@gmail.com"}],"c8":{"exclude":["tests/**"],"reporter":["text","html"]},"dist":{"shasum":"1082dd9af785758d5328a9cde84c2ae77f1ab8ab","tarball":"https://registry.npmjs.org/@aginix/adonis-vulcan-storage/-/adonis-vulcan-storage-0.2.0.tgz","fileCount":47,"integrity":"sha512-nizwCOFd0ZRcH9fJey3eI8zfsr+5bwfua8jV0VJ1ysVS+h5zwW+p4eISZLckry6xN1fvvO8dLlaLPaG6FboX6Q==","signatures":[{"sig":"MEQCIFRW6gJnDz/B6xEMuwyD6c992J43IaZ2j+ZwINZAqaVzAiBSSRNpw61B5O97RiF8If+y+Dy6YZZWp6DbLjs2csYRxg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":99537},"main":"build/index.js","type":"module","types":"./build/index.d.ts","tsdown":{"dts":false,"clean":true,"entry":["./index.ts","./configure.ts","./src/types.ts","./src/define_config.ts","./src/errors.ts","./src/path_validator.ts","./src/security_rules.ts","./src/runtime_state.ts","./src/storage_object.ts","./src/storage_service.ts","./src/storage_objects_controller.ts","./src/validators.ts","./src/routes.ts","./providers/storage_provider.ts","./services/main.ts"],"format":"esm","minify":"dce-only","outDir":"./build","target":"esnext","external":["@adonisjs/core","@adonisjs/core/ace","@adonisjs/core/exceptions","@adonisjs/core/helpers/string","@adonisjs/core/http","@adonisjs/core/providers/vinejs_provider","@adonisjs/core/services/app","@adonisjs/core/services/router","@adonisjs/core/types","@adonisjs/core/commands/configure","@adonisjs/drive","@adonisjs/drive/services/main","@adonisjs/drive/types","@adonisjs/lucid","@adonisjs/lucid/orm","@adonisjs/lucid/types/model","@vinejs/vine","luxon"],"treeshake":false,"sourcemaps":false,"fixedExtension":false},"engines":{"node":">=24.0.0"},"exports":{".":"./build/index.js","./types":"./build/src/types.js","./errors":"./build/src/errors.js","./routes":"./build/src/routes.js","./define_config":"./build/src/define_config.js","./services/main":"./build/services/main.js","./path_validator":"./build/src/path_validator.js","./security_rules":"./build/src/security_rules.js","./storage_object":"./build/src/storage_object.js","./storage_service":"./build/src/storage_service.js","./storage_provider":"./build/providers/storage_provider.js"},"gitHead":"32281a4196e08fcec91a51a0a5b572e68b36c8eb","scripts":{"lint":"eslint .","test":"c8 npm run quick:test","build":"npm run compile","format":"prettier --write .","compile":"tsdown && tsc --emitDeclarationOnly --declaration","pretest":"npm run lint","release":"release-it","version":"npm run build","typecheck":"tsc --noEmit","precompile":"npm run lint","quick:test":"node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts","postcompile":"npm run copy:templates","copy:templates":"copyfiles --up=1 \"stubs/**/*.stub\" build","prepublishOnly":"npm run build"},"_npmUser":{"name":"n3n","email":"nonpawit.tee@gmail.com"},"prettier":"@adonisjs/prettier-config","release-it":{"git":{"push":true,"tagName":"adonis-vulcan-storage-v${version}","commitMessage":"chore(adonis-vulcan-storage): release ${version}","tagAnnotation":"adonis-vulcan-storage-v${version}","requireUpstream":true,"requireCleanWorkingDir":true},"npm":{"publish":true,"skipChecks":true},"github":{"release":true,"releaseName":"@aginix/adonis-vulcan-storage ${version}"},"plugins":{"@release-it/conventional-changelog":{"preset":{"name":"angular"}}}},"_npmVersion":"11.8.0","description":"Drop-in object-storage HTTP module for AdonisJS: signed upload/download URLs, Lucid-backed metadata, path-based security rules, on top of @adonisjs/drive.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^11.0.0","luxon":"^3.7.2","eslint":"^10.0.3","tsdown":"^0.21.0","tempura":"^0.4.1","prettier":"^3.8.1","copyfiles":"^2.4.1","cross-env":"^10.1.0","release-it":"^19.2.4","typescript":"^5.9.3","@types/node":"^25.3.5","@japa/assert":"^4.2.0","@japa/runner":"^5.3.0","@types/luxon":"^3.7.1","@vinejs/vine":"^4.1.0","@adonisjs/core":"^7.0.1","@adonisjs/drive":"^4.0.0","@adonisjs/lucid":"^22.0.0","@poppinss/ts-exec":"^1.4.4","@adonisjs/tsconfig":"^2.0.0","@adonisjs/assembler":"^8.0.0","@adonisjs/eslint-config":"^3.0.0","@adonisjs/prettier-config":"^1.4.5","@release-it/conventional-changelog":"^10.0.5"},"peerDependencies":{"luxon":"^3.0.0","@vinejs/vine":"^4.0.0","@adonisjs/core":"^7.0.0","@adonisjs/drive":"^4.0.0","@adonisjs/lucid":"^22.0.0","@adonisjs/assembler":"^8.0.0"},"peerDependenciesMeta":{"@adonisjs/assembler":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/adonis-vulcan-storage_0.2.0_1778956608010_0.36836500460705146","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@aginix/adonis-vulcan-storage","description":"Drop-in object-storage HTTP module for AdonisJS: signed upload/download URLs, Lucid-backed metadata, path-based security rules, on top of @adonisjs/drive.","version":"0.2.1","engines":{"node":">=24.0.0"},"type":"module","main":"build/index.js","exports":{".":"./build/index.js","./types":"./build/src/types.js","./define_config":"./build/src/define_config.js","./storage_object":"./build/src/storage_object.js","./storage_service":"./build/src/storage_service.js","./security_rules":"./build/src/security_rules.js","./path_validator":"./build/src/path_validator.js","./routes":"./build/src/routes.js","./errors":"./build/src/errors.js","./storage_provider":"./build/providers/storage_provider.js","./services/main":"./build/services/main.js"},"scripts":{"copy:templates":"copyfiles --up=1 \"stubs/**/*.stub\" build","typecheck":"tsc --noEmit","lint":"eslint .","format":"prettier --write .","quick:test":"node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts","pretest":"npm run lint","test":"c8 npm run quick:test","precompile":"npm run lint","compile":"tsdown && tsc --emitDeclarationOnly --declaration","postcompile":"npm run copy:templates","build":"npm run compile","release":"release-it","version":"npm run build","prepublishOnly":"npm run build"},"keywords":["adonisjs","adonis","drive","storage","object-storage","s3","gcs","signed-url","lucid"],"author":{"name":"n3n"},"license":"UNLICENSED","devDependencies":{"@adonisjs/assembler":"^8.0.0","@adonisjs/core":"^7.0.1","@adonisjs/drive":"^4.0.0","@adonisjs/eslint-config":"^3.0.0","@adonisjs/lucid":"^22.0.0","@adonisjs/prettier-config":"^1.4.5","@adonisjs/tsconfig":"^2.0.0","@japa/assert":"^4.2.0","@japa/runner":"^5.3.0","@poppinss/ts-exec":"^1.4.4","@release-it/conventional-changelog":"^10.0.5","@types/luxon":"^3.7.1","@types/node":"^25.3.5","@vinejs/vine":"^4.1.0","c8":"^11.0.0","copyfiles":"^2.4.1","cross-env":"^10.1.0","eslint":"^10.0.3","luxon":"^3.7.2","prettier":"^3.8.1","release-it":"^19.2.4","tempura":"^0.4.1","tsdown":"^0.21.0","typescript":"^5.9.3"},"peerDependencies":{"@adonisjs/assembler":"^8.0.0","@adonisjs/core":"^7.0.0","@adonisjs/drive":"^4.0.0","@adonisjs/lucid":"^22.0.0","@vinejs/vine":"^4.0.0","luxon":"^3.0.0"},"peerDependenciesMeta":{"@adonisjs/assembler":{"optional":true}},"publishConfig":{"access":"public"},"tsdown":{"entry":["./index.ts","./configure.ts","./src/types.ts","./src/define_config.ts","./src/errors.ts","./src/path_validator.ts","./src/proxy_upload_body.ts","./src/security_rules.ts","./src/runtime_state.ts","./src/storage_object.ts","./src/storage_service.ts","./src/storage_objects_controller.ts","./src/validators.ts","./src/routes.ts","./providers/storage_provider.ts","./services/main.ts"],"outDir":"./build","clean":true,"format":"esm","minify":"dce-only","fixedExtension":false,"dts":false,"treeshake":false,"sourcemaps":false,"target":"esnext","external":["@adonisjs/core","@adonisjs/core/ace","@adonisjs/core/exceptions","@adonisjs/core/helpers/string","@adonisjs/core/http","@adonisjs/core/providers/vinejs_provider","@adonisjs/core/services/app","@adonisjs/core/services/router","@adonisjs/core/types","@adonisjs/core/commands/configure","@adonisjs/drive","@adonisjs/drive/services/main","@adonisjs/drive/types","@adonisjs/lucid","@adonisjs/lucid/orm","@adonisjs/lucid/types/model","@vinejs/vine","luxon"]},"release-it":{"git":{"requireCleanWorkingDir":true,"requireUpstream":true,"commitMessage":"chore(adonis-vulcan-storage): release ${version}","tagAnnotation":"adonis-vulcan-storage-v${version}","push":true,"tagName":"adonis-vulcan-storage-v${version}"},"github":{"release":true,"releaseName":"@aginix/adonis-vulcan-storage ${version}"},"npm":{"publish":true,"skipChecks":true},"plugins":{"@release-it/conventional-changelog":{"preset":{"name":"angular"}}}},"c8":{"reporter":["text","html"],"exclude":["tests/**"]},"prettier":"@adonisjs/prettier-config","gitHead":"920cbea1acdaf4398cb35cce3ec02da82636a1c4","types":"./build/index.d.ts","_id":"@aginix/adonis-vulcan-storage@0.2.1","_nodeVersion":"24.13.1","_npmVersion":"11.8.0","dist":{"integrity":"sha512-DlNpyKOEqY1buxlOg7P75m3Dzk6IIZooKj1+oBQPGE3+5rszYFrluCJZaHDrP+bxX+E+TmZA7y8+Js6YWJeb3g==","shasum":"f4bc6b9d54e7562a823acc1bea2dd6d58c67db12","tarball":"https://registry.npmjs.org/@aginix/adonis-vulcan-storage/-/adonis-vulcan-storage-0.2.1.tgz","fileCount":50,"unpackedSize":106173,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCvXybZfkhjSzoV5uiu9w8HoovW8Ce2jeyilwMV3ZYhHQIhANM0Emhdu9S1ru6UVwGoqdZYHpBwZ2azVpjxrybYW9bS"}]},"_npmUser":{"name":"n3n","email":"nonpawit.tee@gmail.com"},"directories":{},"maintainers":[{"name":"n3n","email":"nonpawit.tee@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/adonis-vulcan-storage_0.2.1_1779010675720_0.15607241021553286"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-14T07:42:41.922Z","modified":"2026-05-17T09:37:56.012Z","0.1.0-beta.0":"2026-05-14T07:42:42.175Z","0.1.0":"2026-05-14T18:21:47.185Z","0.2.0":"2026-05-16T18:36:48.150Z","0.2.1":"2026-05-17T09:37:55.872Z"},"author":{"name":"n3n"},"license":"UNLICENSED","keywords":["adonisjs","adonis","drive","storage","object-storage","s3","gcs","signed-url","lucid"],"description":"Drop-in object-storage HTTP module for AdonisJS: signed upload/download URLs, Lucid-backed metadata, path-based security rules, on top of @adonisjs/drive.","maintainers":[{"name":"n3n","email":"nonpawit.tee@gmail.com"}],"readme":"# @aginix/adonis-vulcan-storage\n\n> Drop-in object-storage HTTP module for AdonisJS. Signed upload + download URLs, Lucid-backed metadata, path-based security rules. Wraps `@adonisjs/drive` so consumers never call `drive.use()` themselves.\n\n## What you get\n\n- **A `StorageObject` Lucid model** with built-in disk helpers\n  ```ts\n  const object = await StorageObject.findOrFail(id)\n  const url = await object.getSignedDownloadUrl()      // signed download URL\n  await object.deleteFromStorage()                      // remove from disk\n  ```\n- **A ready-made HTTP API** mounted at `/storage/v1`:\n  - `POST   /objects/upload` — reserve a path, return a signed upload URL\n  - `GET    /objects/:id` — fetch metadata\n  - `GET    /objects/:id/sign` — issue a signed download URL\n  - `DELETE /objects/:id` — remove the row + the underlying blob\n  - `PUT    /upload?token=...` — proxy upload (auto-engages when the disk can't sign; row id lives in the encrypted token)\n  - `GET    /download?token=...` — proxy download (same model)\n- **Disk- and path-based security rules** with `:param` and `**` placeholders, scoped per Drive disk\n- **Migration auto-published** via `node ace configure` — just run `migration:run`\n- **Zero scaffolding required**: import `StorageObject` from the package directly. Subclass only if you want extra columns or relations.\n\n## Install\n\n```sh\nnpm install @aginix/adonis-vulcan-storage @adonisjs/drive @adonisjs/lucid\nnode ace configure @aginix/adonis-vulcan-storage\nnode ace migration:run\n```\n\nThe configure hook publishes three files:\n\n- `config/storage.ts` — module config\n- `start/storage.ts` — routes + security rules (this is where you wire your auth middleware and define which paths users can read/write)\n- `database/migrations/{ts}_create_storage_objects_table.ts` — the `storage_objects` table\n\nand adds two entries to `adonisrc.ts`:\n\n- `'@aginix/adonis-vulcan-storage/storage_provider'` provider\n- `'#start/storage'` preload file\n\n## Required setup\n\nAfter `configure` you still need to do four things before uploads work end-to-end. The first two are non-negotiable; the last two only matter if you upload `text/*` payloads or run against an internally-deployed S3.\n\n### 1. Pick a Drive disk in `config/drive.ts`\n\nThis module never calls `drive.use()` itself — it always reads a disk name from a row or from `config/storage.ts`. So you need at least one disk configured via `@adonisjs/drive`:\n\n```ts\n// config/drive.ts\nexport default defineConfig({\n  default: env.get('DRIVE_DISK'),\n  services: {\n    s3: services.s3({\n      bucket: env.get('S3_BUCKET'),\n      // ... credentials, region, endpoint\n    }),\n    fs: services.fs({ location: app.makePath('storage'), visibility: 'public' }),\n  },\n})\n```\n\n### 2. Set `defaultDisk` in `config/storage.ts`\n\n`defaultDisk` is the Drive disk name used when an upload doesn't pick one explicitly. It must match a key from `config/drive.ts`. Everything else has sane defaults — see [Configuration](#configuration).\n\n```ts\n// config/storage.ts\nexport default defineConfig({\n  defaultDisk: env.get('DRIVE_DISK'),\n})\n```\n\n### 3. Wire auth middleware + security rules in `start/storage.ts`\n\nWithout any `defineRule()` call, the module denies every operation (fail-closed). The simplest viable setup mounts the routes inside your `auth()` middleware so `ctx.auth.user` is available to your rule handlers:\n\n```ts\nimport { defineRule } from '@aginix/adonis-vulcan-storage'\nimport { storageRoutes } from '@aginix/adonis-vulcan-storage/routes'\nimport { middleware } from './kernel.js'\n\nstorageRoutes({\n  prefix: 'storage/v1',\n  middleware: [middleware.auth()],\n})\n\ndefineRule('s3/users/:userId/files/**', {\n  read:   async ({ auth }, params) => String(auth.user?.id) === params.userId,\n  create: async ({ auth }, params) => String(auth.user?.id) === params.userId,\n  delete: async ({ auth }, params) => String(auth.user?.id) === params.userId,\n  maxDownloadTtlSeconds: 60 * 60,\n})\n\ndefineRule('*/public/**', {\n  read: async () => true,\n})\n```\n\n> [!important]\n> `storageRoutes` is imported from `@aginix/adonis-vulcan-storage/routes` (a subpath), not from the package root. The barrel can't re-export it because `@adonisjs/core/services/router` runs a top-level `await app.booted()` that crashes during `node ace configure`. The publishable `start/storage.ts` stub already uses the right path; only deviate if you know why.\n\n**Rule template format** is `<disk>/<path>`. The disk segment:\n\n- a literal disk name (e.g. `s3`) — matches that exact disk\n- `:name` — captures the disk name into `params.name`\n- `*` — matches any disk (`params.disk` still gets the matched name)\n\nThe path segment supports `:name` (single segment, exposed as `params.name`) and `**` (matches any suffix at the end).\n\nRules are evaluated in registration order; the first matching template wins. A missing handler for an action denies. Without any rules every action is denied — the module is fail-closed.\n\n### 4. Disable bodyparser's `raw` parser if you upload `text/*` content\n\nAdonis bodyparser's default `raw.types` is `['text/*']` — every `text/html`, `text/csv`, `text/plain` upload is consumed and stashed as a UTF-8 string before any route handler runs. The storage controller falls back to that cached text, so ASCII-compatible uploads still work, but the round-trip through `Buffer.from(str, 'utf8')` corrupts non-UTF-8 bytes (TIS-620, Windows-874, Latin-1, mixed-encoding CSVs from legacy Excel exports).\n\nIf you upload any text content, override the default in `config/bodyparser.ts`:\n\n```ts\nexport default defineConfig({\n  // ... rest of your config\n  raw: { types: [] },   // give the storage proxy route the raw bytes\n})\n```\n\nWith that off, the proxy upload route reads bytes straight from the request stream — byte-exact regardless of `Content-Type`.\n\n### 5. Force proxy mode for internally-deployed S3-compatible disks\n\nOptional, only relevant if you run rustfs / minio / ceph behind an internal endpoint. See [Proxy mode for internally-deployed S3-compatible disks](#proxy-mode-for-internally-deployed-s3-compatible-disks).\n\n## Paths\n\nObject paths are validated server-side at reservation time. The character set matches flydrive's exactly — anything outside fails fast with `E_INVALID_STORAGE_PATH` (HTTP 400) before any DB row is created:\n\n- letters, digits, `-`, `_`, `!`, `/`, `.`, and whitespace\n- no leading or trailing `/`\n- no `.` / `..` segments (path-traversal guard)\n- max 1024 chars\n- no control characters\n\nIf your users supply filenames, sanitize on the client. A simple `name.replace(/[^A-Za-z0-9\\-_!.\\s]/g, '_')` is enough for the common cases (parens, brackets, unicode, URL specials → `_`); see `apps/demo/inertia/pages/posts/index.tsx` for a worked example.\n\n## Use it from your code\n\n### Model utilities\n\n`StorageObject` lives behind a subpath export (so the configure hook doesn't drag `@adonisjs/lucid` / `@adonisjs/drive` / `luxon` into its import chain when those peers aren't installed yet):\n\n```ts\nimport StorageObject from '@aginix/adonis-vulcan-storage/storage_object'\n\nconst object = await StorageObject.findOrFail(id)\n\nconst downloadUrl = await object.getSignedDownloadUrl({ expiresIn: '5 mins' })\nconst uploadUrl   = await object.getSignedUploadUrl()\nconst metadata    = await object.getStorageMetadata()\nconst exists      = await object.existsInStorage()\n\nawait object.deleteFromStorage()   // only the blob; keeps the row\nawait object.delete()              // only the row; keeps the blob\n```\n\nThe model picks the right disk based on the object's `disk` column — no `drive.use()` needed.\n\n### Service-layer orchestration (with authorization)\n\n```ts\nimport storage from '@aginix/adonis-vulcan-storage/services/main'\n\n// Issue a signed download with auth + rule enforcement\nconst { url, expiresInSeconds } = await storage.requestDownload(ctx, objectId)\n\n// Reserve an upload (validates path, authorizes 'create', upserts the row)\nconst { object, uploadUrl } = await storage.createUpload({\n  ctx,\n  path: `users/${ctx.auth.user!.id}/avatar.png`,\n  name: 'avatar.png',\n  contentType: 'image/png',\n})\n\n// Authorize 'delete' + remove row + best-effort blob cleanup\nawait storage.delete(ctx, objectId)\n```\n\n## Configuration\n\n`config/storage.ts`:\n\n```ts\nimport env from '#start/env'\nimport { defineConfig } from '@aginix/adonis-vulcan-storage'\n\nexport default defineConfig({\n  defaultDisk: env.get('DRIVE_DISK'),\n  uploadUrlTtl: '15 mins',\n  downloadUrlTtlDefault: '15 mins',\n  downloadUrlTtlMaxSeconds: 60 * 60 * 24 * 7,\n  lastAccessedThrottleHours: 1,\n})\n```\n\n| Field | Default | Notes |\n|---|---|---|\n| `defaultDisk` | (required) | Drive disk name used when an upload omits `disk`. |\n| `uploadUrlTtl` | `'15 mins'` | TTL for signed upload URLs. |\n| `downloadUrlTtlDefault` | `'15 mins'` | Default TTL for signed download URLs. |\n| `downloadUrlTtlMaxSeconds` | `60 * 60 * 24 * 7` | Hard ceiling on download URL TTL. Rules can clamp further via `maxDownloadTtlSeconds`. |\n| `lastAccessedThrottleHours` | `1` | Min hours between `lastAccessedAt` writes per object. Set `0` to disable. |\n| `proxyOnlyDisks` | `[]` | Disk names whose signed URLs aren't reachable from the client (e.g. internal `rustfs` / `minio`). Listing a disk here forces proxy mode regardless of the driver's signing capability. |\n\n## Customizing\n\n### Custom routes\n\n`storageRoutes({...})` returns a route group, so you can chain or wrap it:\n\n```ts\nstorageRoutes({\n  prefix: 'api/files',\n  middleware: [middleware.auth(), middleware.throttle()],\n})\n```\n\n### Custom controller\n\n```ts\nstorageRoutes({\n  controller: () => import('#controllers/my_storage_controller'),\n})\n```\n\n### Custom service\n\nRegister your subclass in your own provider's `register()` — it overrides the package binding:\n\n```ts\nimport { StorageService } from '@aginix/adonis-vulcan-storage'\n\nclass MyStorageService extends StorageService {\n  protected resolveUserId(ctx) {\n    return ctx._user?.id ?? null   // your auth shape\n  }\n}\n\n// in your provider:\nthis.app.container.singleton('storage', () => new MyStorageService())\n```\n\n### Custom model (extra columns / relations)\n\n```ts\nimport StorageObject from '@aginix/adonis-vulcan-storage/storage_object'\nimport { belongsTo } from '@adonisjs/lucid/orm'\nimport type { BelongsTo } from '@adonisjs/lucid/types/relations'\nimport User from '#models/user'\n\nexport default class AppStorageObject extends StorageObject {\n  @belongsTo(() => User, { foreignKey: 'createdBy' })\n  declare creator: BelongsTo<typeof User>\n}\n```\n\n(The `storage_objects` table already exists; just add columns/FKs via your own migration if needed.)\n\n## Content validation\n\nThe package doesn't ship content validation — it trusts what your security rules let through. Plug your own logic in at one of three points, picking based on what you can know *when*:\n\n### Claim-based, at reserve time\n\nThe `create` handler in `defineRule` receives the full request context, so you can reject uploads on the client-supplied `contentType` and `size` before any DB row is created or any byte is transferred:\n\n```ts\nconst ALLOWED_MIMES = ['image/png', 'image/jpeg', 'application/pdf']\nconst MAX_BYTES = 10 * 1024 * 1024\n\ndefineRule('s3/users/:userId/posts/**', {\n  create: async ({ auth, request }, params) => {\n    if (String(auth.user?.id) !== params.userId) return false\n    const { contentType, size } = request.body() as { contentType?: string; size?: number }\n    if (!contentType || !ALLOWED_MIMES.includes(contentType)) return false\n    if (typeof size === 'number' && size > MAX_BYTES) return false\n    return true\n  },\n  read: ...,\n  delete: ...,\n})\n```\n\nCheapest by far, but the client can lie about both fields. Use this as the first gate, not the only one.\n\n### Server-side, at proxy upload\n\nFor the proxy upload path (FS in dev, disks listed in `proxyOnlyDisks`), subclass `StorageService` and inspect the buffered body before it's written:\n\n```ts\nimport { StorageService } from '@aginix/adonis-vulcan-storage/storage_service'\n\nclass MyStorageService extends StorageService {\n  async proxyUpload(token, file) {\n    // (Token + row resolution happen inside super.proxyUpload —\n    // for finer control, copy that prologue here.)\n    if (file.stream || file.rawBody) {\n      const body = await readBuffer(file)   // your helper or use a small lib\n      if (!magicBytesLookOk(body, file.contentType)) {\n        throw new E_STORAGE_AUTHORIZATION()\n      }\n    }\n    return super.proxyUpload(token, file)\n  }\n}\n```\n\nWire your subclass via the `'storage'` container binding ([see Custom service](#custom-service)).\n\nThis catches real bytes, but **only for proxy mode** — direct-to-S3 signed uploads (the production hot path) skip your server entirely. Treat this as a dev / staging safety net, not a primary defense.\n\n### Post-upload async, universal\n\nHook into the model's `@afterCreate` lifecycle and queue a background job that reads the object back from storage:\n\n```ts\nimport { afterCreate } from '@adonisjs/lucid/orm'\nimport StorageObject from '@aginix/adonis-vulcan-storage/storage_object'\nimport queue from '#services/queue'\n\nexport default class AppStorageObject extends StorageObject {\n  @afterCreate()\n  static enqueueValidation(row: AppStorageObject) {\n    void queue.add('storage:validate', { objectId: row.id })\n  }\n}\n```\n\n```ts\n// worker handler\nconst obj = await AppStorageObject.findOrFail(payload.objectId)\nconst bytes = await obj.getDisk().getBytes(obj.path)\nif (!(await scanClean(bytes))) {\n  await obj.deleteFromStorage()   // best-effort; ignores if already gone\n  await obj.delete()\n  return\n}\nobj.metadata = { ...obj.metadata, validated: true }\nawait obj.save()\n```\n\nWorks regardless of upload mode (proxy or direct-to-S3), runs off the request path, and the row's `metadata.validated` flag gives the rest of your app a single fact to check before exposing the object. The tradeoff is the validation window: malicious content sits in storage between upload completion and the job running — never present it to other users until `metadata.validated === true`.\n\n### Recommendation\n\nCombine **claim-based** (cheap front gate via security rule) with **post-upload async** (the universal defense). The server-side proxy-upload hook is useful in dev and staging when you don't want to wire a queue, but don't rely on it in production: real uploads will bypass it via signed URLs.\n\n## Drivers that can't sign URLs (FS in dev)\n\nThe bundled `@adonisjs/drive` FS driver doesn't support signed URLs — `getSignedUploadUrl()` / `getSignedUrl()` throw `E_CANNOT_GENERATE_URL`. Rather than fail your local dev, the service catches that specific error and mints an **encrypted, time-bounded, purpose-scoped token** instead, then returns a same-origin URL with the token in the query string. The behavior matches how S3/GCS signed URLs work:\n\n| | Signed mode (S3 / GCS) | Proxy mode (FS) |\n|---|---|---|\n| `uploadUrl` / `url` returned | external signed URL | `/storage/v1/upload?token=...` or `/storage/v1/download?token=...` |\n| Authorization | URL signature | encrypted token in query (`adonis-vulcan-storage:upload` / `:download` purposes) |\n| Object identity | encoded in the signature | encoded in the encrypted token (no `:id` URL segment) |\n| Client headers required | none | none |\n\nThe two proxy endpoints are registered **outside** your middleware group on purpose — the token IS the auth. So `<img src=\"...\">` and `window.location` work directly with no header juggling, and clients (`@aginix/vulcan-js`) PUT raw bytes the same way they would to a signed S3 URL. The response carries an informational `proxied: boolean` field, but the SDK doesn't act on it — the URL is self-describing.\n\n> [!note]\n> The proxy is a correctness fallback, not a performance choice. For prod, configure S3 / GCS / a signing-capable FS driver — your app server stays out of the data path.\n\n### Proxy mode for internally-deployed S3-compatible disks\n\nAuto-fallback only triggers when the driver throws `E_CANNOT_GENERATE_URL`. An S3-compatible disk (rustfs, minio, ceph) deployed on an internal network signs fine — but the URL it produces points at an internal host the browser can't reach. Tell the module to skip the signed-URL fast path for those disks via `proxyOnlyDisks`:\n\n```ts\nexport default defineConfig({\n  defaultDisk: env.get('DRIVE_DISK'),\n  proxyOnlyDisks: ['rustfs'],\n})\n```\n\nThe disk still works exactly like a signing-capable disk from the SDK's perspective — only the URL it gets back happens to be same-origin with a token. Use this for staging/preview environments where storage isn't publicly exposed.\n\n## Errors\n\nAll errors are AdonisJS `Exception` subclasses and render to the right HTTP status automatically.\n\n| Class | Status | Code |\n|---|---|---|\n| `E_INVALID_STORAGE_PATH` | 400 | `E_INVALID_STORAGE_PATH` |\n| `E_STORAGE_AUTHORIZATION` | 403 | `E_STORAGE_AUTHORIZATION` |\n| `E_STORAGE_OBJECT_NOT_FOUND` | 404 | `E_STORAGE_OBJECT_NOT_FOUND` |\n\n## License\n\nProprietary — see [LICENSE.md](./LICENSE.md).\n","readmeFilename":"README.md"}