{"_id":"@3sln/trove","_rev":"17-3776caa3fcd16fbc41fef902898a0362","name":"@3sln/trove","dist-tags":{"latest":"0.0.20"},"versions":{"0.0.2":{"name":"@3sln/trove","version":"0.0.2","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.2","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"7a03995eb6e407d6a9492db0456d012d48832261","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.2.tgz","fileCount":162,"integrity":"sha512-3o+LJ4UEHsUHxh8jqmWcbMStjPNNXVMdS34D63NeJG2gpFKTHnpgSkifXflVxHFGdx1+H7MHdXwqpwiQSIYo9Q==","signatures":[{"sig":"MEYCIQDFC9h2rR0kp/pqGZK3Jh7ZlT5lyEijgjyMc8hQdpoohQIhAKN0ZSp0DeZT9NaG8JjpCX17umLLRfaE5X76WXXrvwUA","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3556913},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"1c5cd2262ae1878cc147cdea1533e6901436063e","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.2_1785281464127_0.5674246334143922","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@3sln/trove","version":"0.0.3","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.3","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"a657d1bd0ce8eec69944055e663caaa3401c9c56","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.3.tgz","fileCount":162,"integrity":"sha512-Oz97KV/zGxD+nnnRRjfGtKca2eUVrOK4+biltJZqbf1S880Ag6ovL4UleYxJ1nNv+z/qeVl2AnWjJOCZFdj0AQ==","signatures":[{"sig":"MEUCIQCKoN7swYXvjaxEzS9meCa5U00SMa7bHYJc9mK/AY0E8AIgPeVxqpD9DmjstN9QSMnsllJRgx5tlG7Xc2MFU7H7sbw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3558335},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"bed04d6da74464289c4a476f78a4b38e8467f5b6","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.3_1785282619414_0.7290284708289854","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"name":"@3sln/trove","version":"0.0.4","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.4","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"e5d43d434e3bae263124822d4eb5e21ddc39a7ca","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.4.tgz","fileCount":163,"integrity":"sha512-s+2shsEBUtLWsaQzAf+tPYJjssaaF8vrCHknH4N2wd+6HO3qRomFN8mTTJKroZdFNturnHP5/7dUWOVzbZZQFA==","signatures":[{"sig":"MEUCICWf7RpkGz4PPeRC1sDEVIF8u2MNLKniTrA00ZERVLiNAiEAy1hQgnFT+ffqRg/9A1WSKmtvLp3cwQsg5E50cj3ruDQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3576149},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"5a8d1405270dec830945c375127db790eefcd295","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.4_1785303638667_0.4202338905715204","host":"s3://npm-registry-packages-npm-production"}},"0.0.5":{"name":"@3sln/trove","version":"0.0.5","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.5","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"cd197bc97dc8a37d5232e41f0970849046ee3ecb","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.5.tgz","fileCount":168,"integrity":"sha512-HsYPqxUtS87MFhYuFOvxCXSUzShPGTVIakKghu2EtvEN+Kn2mb4HNW+/8mDWok5M5GeVvlpS2Kpag0E9LPQv3w==","signatures":[{"sig":"MEYCIQD0Yh4xgYv6w2hTtOTRaVfEVAMrtu9n1HjLoPLxrZjP2AIhALjbBuOftwOA6KVnQG6M18XpE+cTgp13Ka0C8U69Engv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.5","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3730149},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"d7a1409a856f59333b5a902a84d115273b7e66f2","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.5_1785386280301_0.9995760179956379","host":"s3://npm-registry-packages-npm-production"}},"0.0.7":{"name":"@3sln/trove","version":"0.0.7","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.7","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"1212f396f76dfaf1b35b8e9213aec27a469174c0","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.7.tgz","fileCount":168,"integrity":"sha512-WMFJI2zcYUHtsMGNtlfQuo0mIgG1rtxdX1u9e1zJbA6FHIPLFVg3yiDJs2Tn+UDKr6Zv4T/EG+dHU1aVsQtbhQ==","signatures":[{"sig":"MEUCIEuau57Fg75rMkMHfcaJu6YE0w+DZVyZB3bITNgN9SrYAiEAgi5vexzCmaz4IxSlOm1LLq2cayjgF1znmiUYQZI/dy4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.7","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3735029},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"27a704357f035b20502a8f6fa539a3032815569e","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.7_1785387532203_0.7622492769723399","host":"s3://npm-registry-packages-npm-production"}},"0.0.8":{"name":"@3sln/trove","version":"0.0.8","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.8","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"2b40af2a3a9ae8d72771191e7cbd12d3f250383e","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.8.tgz","fileCount":174,"integrity":"sha512-Ij1kAJMSuhDwR8oe8PN6Nbhaxkjb/MKdjGWK5BHzC6Pnn0F6tLGXNASWXT+xQcOfrNZrq3cAAgzcoG462KMU5Q==","signatures":[{"sig":"MEYCIQCuO/2R1mT6PP5ElymOZzmd/K+oipmTvI1EpLTPYPcNqAIhAK4DTrulQM/dBcsTKJkOA1wB5woY3E4U8qHu+QO2Ds/l","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.8","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3880878},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"38c5b3a2fe4be835f6f4c4ca5e97020a24e5046f","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.8_1785442053595_0.8704434637216341","host":"s3://npm-registry-packages-npm-production"}},"0.0.9":{"name":"@3sln/trove","version":"0.0.9","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.9","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"6a3c513d917e5d6a16e1f91f60a77bcff686a730","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.9.tgz","fileCount":188,"integrity":"sha512-GPiQpgQs1tCf/XCDaxGU7dWhE4wLcBrhF/Oob8rCbDTJV88YNBqsVzc4Gwz89XkFSRSLfSQmTNSOJMv4O7AEMQ==","signatures":[{"sig":"MEQCIGdml5KBkVDJYcIiqdwC8SvgMNwDpHSoRUEPypchHh7TAiApg9WqF9sYIwDKBTDjwRIoD5Vgsw4GV+YLhV68nQxm6g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.9","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4278338},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"63dd98ecd1ddacccc85bb1f2c41af9e13d19ba92","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.9_1785599642159_0.40094523539792615","host":"s3://npm-registry-packages-npm-production"}},"0.0.10":{"name":"@3sln/trove","version":"0.0.10","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.10","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"71c3b0f5a238f8d392194753e19d2918c57aa5a0","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.10.tgz","fileCount":188,"integrity":"sha512-wbiXbGdeeuNJg6qAeMOjhI4XVRwCpsBmhDrpELdySofLMfEDzlPp4tTtaXYWzuIiD/7AQbDTxV8Ub5taO6w18g==","signatures":[{"sig":"MEYCIQDZ1P7wdUWhr5p5gwbl9qzmpmT9NOO7c2Q8i/3XU5DXHAIhAPV/A4x+Bk8OYx3vCGvkuGpS6uFUjUZ5uzc7YpPhn+qs","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.10","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4281898},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"cca889ce568798574a0f6dbf44f88e045feb4264","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.10_1785603174286_0.7927320704577032","host":"s3://npm-registry-packages-npm-production"}},"0.0.11":{"name":"@3sln/trove","version":"0.0.11","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.11","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"99cbdf0fd94a15b5db4681d976625ba39de24dc3","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.11.tgz","fileCount":190,"integrity":"sha512-VeGvwW/i+vcEWyykDvu3EwraNnzECP8plEq3wIfAypQCvp7u8xAMoLP9YAZ9OI4FhtY5vdDYc3KW3LIHYUyYRg==","signatures":[{"sig":"MEYCIQC/puKo7PA5Xmz+pqUPT9XL/cIz2B5zbstpkWTMZK7IogIhAKOm5uR5m3kdXF3MPRIItvnHYodsqTwQgDIJixBmsY+f","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.11","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4338389},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"1a33686277d196df92ca1177bc0003095e5c4c79","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.11_1785613647872_0.5265584466981639","host":"s3://npm-registry-packages-npm-production"}},"0.0.12":{"name":"@3sln/trove","version":"0.0.12","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.12","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"7e7275fc1df2495c5084fcc9c981d556b7588e2f","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.12.tgz","fileCount":190,"integrity":"sha512-+4oQCS0xPu3b5dXfyMwzrmeYvXwJVZjYxby4HbHclS1aNbtioywyEPkOmSXAG8odrJN134IIfc9b2hzGoo9Now==","signatures":[{"sig":"MEUCIGcR8zypXy+DGVHum3WwTAMaqkpGDCjf92sAkP1QLJnvAiEAqWA3YKqVtzEVvdCutvKNWdX0SDzxpZLIT78I0jpxihs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.12","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4340272},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"ccba2eeb8e1790bc4c049309ffab4dc7ffb55af1","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.12_1785625012970_0.5538820651871963","host":"s3://npm-registry-packages-npm-production"}},"0.0.13":{"name":"@3sln/trove","version":"0.0.13","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.13","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"64820ead282928c8f040ead9b4ed09c270e5b005","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.13.tgz","fileCount":193,"integrity":"sha512-HrIakIpli6m57Ej0jwpfR7TXEaKbWLN/mUUC1bwjreJtYKeCsaFpUYWhf8D+iEr6ygnKs4IziaYYxOtx+F3Omg==","signatures":[{"sig":"MEYCIQDctLCnXjELV5JZxw34ZKdGyd3zZZcxGOgOh/50pbltnQIhAIlF+4MHRPJd5IrBK+zp6lzVSGXD9SU7nToHel6zaQJD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.13","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4475112},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"50e13099e578209e67971dc4537ebefcd35ebb16","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs","build:plugins":"bun plugins/build.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.13_1785637315495_0.29966327844467244","host":"s3://npm-registry-packages-npm-production"}},"0.0.14":{"name":"@3sln/trove","version":"0.0.14","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.14","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"775d30292d524df39864a02fd41f5bf0504e23ba","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.14.tgz","fileCount":193,"integrity":"sha512-UMUchtXnzKmeOSFginq/RAHukHKv9SHgVK6t16lvMJDUYg8SbfL3dcHC3FsjDICEZPtYt1R5eC8V9xAfKrRYyQ==","signatures":[{"sig":"MEUCIAT68g64YUhpNSWp6ZuWwFxjjPsfwW5uPl90HehMABKUAiEAndeJIMvSsyFMcP01cVZAHQeQMprhNqCa7B5vZ32tpTM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.14","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4475112},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"545039f7e995cace41300e1dfb81ff84db57b41f","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs","build:plugins":"bun plugins/build.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.14_1785644068017_0.48232799480307564","host":"s3://npm-registry-packages-npm-production"}},"0.0.16":{"name":"@3sln/trove","version":"0.0.16","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.16","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"e744bde530d1eac99a9c3402e32e988dd95ea2d9","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.16.tgz","fileCount":193,"integrity":"sha512-rsg1Bd5ybXlDemmSmKEI0cAf5pzdrzFYpfvtFAflaHepkSy+957rPS7oZv6UiWpK4iXS//Vt0pq80uaCO1FhIQ==","signatures":[{"sig":"MEQCIExERQQ23vvv4PjwYJ73GUyZlRrJk3VZf5aJMsZKqaHDAiBxfOaxl8cLU5nknxArxdED0Up18X81wDZz0L3FSyzZ5g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.16","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4486412},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"bd824a8e16f7e6ce8f45713b13ccc8a88bb77e5a","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs","build:plugins":"bun plugins/build.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.16_1785644933437_0.2947759087248609","host":"s3://npm-registry-packages-npm-production"}},"0.0.17":{"name":"@3sln/trove","version":"0.0.17","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.17","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"eda38f0cca3796ec14bd1c1d59b3a2623c893100","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.17.tgz","fileCount":194,"integrity":"sha512-LqTWe3ETtpiWwEEUtl7az7vPO/CT6HpsmI/QsZwyJJBHMzMpI9zBIzBAubNbJYnoCsJmo6QbKHF5zgtOAVLW2g==","signatures":[{"sig":"MEYCIQC0nURpWR1wIPN6IF3X0PtwWChk+PiVRX/vrnzEdKshPgIhAKz0VC2QDBVvnc+cAmdoNtm3oD9ZxYfsSxVXSpfr/Rz7","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.17","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4509765},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"a8b1bff7e892d918bf25a8ec7e33817c1f56152a","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs","build:plugins":"bun plugins/build.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.17_1785648644846_0.04118029300254733","host":"s3://npm-registry-packages-npm-production"}},"0.0.18":{"name":"@3sln/trove","version":"0.0.18","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.18","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"3b50c110e32f3ceba5c71534bcbf1c497c667308","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.18.tgz","fileCount":195,"integrity":"sha512-YlNbtpY/3T506W52nWtTMIGkXqh3HhTdW6c7AgdKl703BNx6Rzf4rVOy9LSmkioC+J98OjYiiAVSSxWhwQTO4Q==","signatures":[{"sig":"MEUCICV8+1Wbs8RveKo6oZk+WJjxtqIaSZMNGbmjpPByGDm0AiEArrkik+QYGrPSQbAn9Tgm3J0QQoDJehClu0mGSL2jcBk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.18","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4579922},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"774123226335edd7a7a415af9586556b214826d2","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs","build:plugins":"bun plugins/build.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.18_1785708231184_0.5993419694488726","host":"s3://npm-registry-packages-npm-production"}},"0.0.19":{"name":"@3sln/trove","version":"0.0.19","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/trove@0.0.19","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/trove#readme","bugs":{"url":"https://github.com/3sln/trove/issues"},"dist":{"shasum":"f47d95b02f0790e450c08b14da975574836358a0","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.19.tgz","fileCount":195,"integrity":"sha512-N2gRrAQFLp286Kce6HHE0R+8/szL6BllB2+hTDA3Lwaf5drepl/NNLunKXw1G10KAD5BDCt3Lh0KG8rmrmqf8w==","signatures":[{"sig":"MEQCIEk1PvBdDqLZpSLwIEkynJwKYY6DcCDZ3RArhVmdIV7WAiAouQUpinlku6+rwpmpCXTa30mIS0j+rk+HS5LUkx4YvA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.19","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4581852},"type":"module","engines":{"bun":">=1.1.0","node":">=20"},"exports":{".":"./packages/server/src/index.js","./core":"./packages/core/src/index.js","./web/*":"./packages/web/src/*","./core/*":"./packages/core/src/*","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./package.json":"./package.json","./plugin-sdk/*":"./packages/plugin-sdk/src/*"},"gitHead":"ed3612ffe2476b409b1b40d9fe3cfacc96fa8acf","scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","test":"bun test","serve":"bun packages/server/src/adapters/bun.js","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","version":"npm run sync-version && git add packages/create-trove/package.json","build:web":"cd packages/web && bun build.mjs","serve:node":"node packages/server/src/adapters/node.js","sync-version":"node scripts/sync-version.mjs","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs","build:plugins":"bun plugins/build.mjs"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/trove.git","type":"git"},"_npmVersion":"10.9.8","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.3","@3sln/ngin":"^0.0.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"aws4":"^1.13.2","s3rver":"^3.7.1","sql.js":"^1.14.1","@3sln/dodo":"^0.0.10","@web/dev-server":"^1.0.0","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","@web/test-runner":"^1.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"_npmOperationalInternal":{"tmp":"tmp/trove_0.0.19_1785708619240_0.6258466701858612","host":"s3://npm-registry-packages-npm-production"}},"0.0.20":{"name":"@3sln/trove","version":"0.0.20","type":"module","description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","repository":{"type":"git","url":"git+https://github.com/3sln/trove.git"},"license":"MIT","author":{"name":"Ray Stubbs"},"keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"publishConfig":{"access":"public"},"exports":{".":"./packages/server/src/index.js","./server":"./packages/server/src/index.js","./server/*":"./packages/server/src/*","./core":"./packages/core/src/index.js","./core/*":"./packages/core/src/*","./plugin-sdk":"./packages/plugin-sdk/src/index.js","./plugin-sdk/*":"./packages/plugin-sdk/src/*","./web/*":"./packages/web/src/*","./package.json":"./package.json"},"scripts":{"dev":"web-dev-server --config packages/web/web-dev-server.config.mjs","build:web":"cd packages/web && bun build.mjs","build:plugins":"bun plugins/build.mjs","serve":"bun packages/server/src/adapters/bun.js","serve:node":"node packages/server/src/adapters/node.js","test":"bun test","test:browser":"web-test-runner --config packages/web/web-test-runner.config.mjs","backup":"bun scripts/backup.mjs","prepack":"bun run build:web","sync-version":"node scripts/sync-version.mjs","version":"npm run sync-version && git add packages/create-trove/package.json"},"engines":{"node":">=20","bun":">=1.1.0"},"dependencies":{"@3sln/ngin":"^0.0.4","fflate":"^0.8.3"},"optionalDependencies":{"sqlite-vec":"^0.1.9"},"devDependencies":{"@3sln/dodo":"^0.0.10","@modelcontextprotocol/sdk":"^1.29.0","@web/dev-server":"^1.0.0","@web/test-runner":"^1.0.0","aws4":"^1.13.2","es-module-lexer":"^1.7.0","playwright-core":"^1.61.1","s3rver":"^3.7.1","sql.js":"^1.14.1"},"_id":"@3sln/trove@0.0.20","gitHead":"4a409bb2055316c1cd53cb4396b3be9a7c9cc1cb","bugs":{"url":"https://github.com/3sln/trove/issues"},"homepage":"https://github.com/3sln/trove#readme","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-kDkNelizdfeFPZEblIp0m/qsz/oOoH6QR5l0wjaEKuJ170xs0/ragKfWvhGCViLsX6Bhpmq/JdUnwWhZ8o+8Mg==","shasum":"d77b125a805a0752a5c0b08ce8a76e0b1a273693","tarball":"https://registry.npmjs.org/@3sln/trove/-/trove-0.0.20.tgz","fileCount":196,"unpackedSize":4628343,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2ftrove@0.0.20","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDd3BUaK0TKlDSH+x9qxwy17QWkFf2Xz/lbiGbKDeYPKgIgZo8a2bQ5mjDR38hbgBJ9FQ4tXUunfoeAJ80zLRqoDns="}]},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"directories":{},"maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/trove_0.0.20_1785736048165_0.5707948004927692"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-28T23:31:03.964Z","modified":"2026-08-03T05:47:28.652Z","0.0.2":"2026-07-28T23:31:04.327Z","0.0.3":"2026-07-28T23:50:19.592Z","0.0.4":"2026-07-29T05:40:38.846Z","0.0.5":"2026-07-30T04:38:00.489Z","0.0.7":"2026-07-30T04:58:52.362Z","0.0.8":"2026-07-30T20:07:33.840Z","0.0.9":"2026-08-01T15:54:02.378Z","0.0.10":"2026-08-01T16:52:54.439Z","0.0.11":"2026-08-01T19:47:28.072Z","0.0.12":"2026-08-01T22:56:53.166Z","0.0.13":"2026-08-02T02:21:55.661Z","0.0.14":"2026-08-02T04:14:28.190Z","0.0.16":"2026-08-02T04:28:53.586Z","0.0.17":"2026-08-02T05:30:45.115Z","0.0.18":"2026-08-02T22:03:51.430Z","0.0.19":"2026-08-02T22:10:19.450Z","0.0.20":"2026-08-03T05:47:28.353Z"},"bugs":{"url":"https://github.com/3sln/trove/issues"},"author":{"name":"Ray Stubbs"},"license":"MIT","homepage":"https://github.com/3sln/trove#readme","keywords":["drive","file storage","semantic search","self-hosted","plugins","s3"],"repository":{"type":"git","url":"git+https://github.com/3sln/trove.git"},"description":"Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"readme":"# 🗄️ Trove\n\nA **self-hostable Google Drive** you actually own — with **semantic search**,\n**pluggable storage** (S3 / filesystem / NAS), a **search-first workbench**, and\na **sandboxed plugin system**. Ships as a runtime-agnostic library plus a server\nthat speaks plain `Request → Response`, so it runs on **Node**, **Bun**, or\n**Cloudflare Workers** with a light wrapper.\n\nBuilt on the [3sln stack](https://github.com/3sln/stack): **ngin** (DI / CQRS) and\n**dodo** (functional VDOM, with its own reactive cells).\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│  @3sln/trove/web     search-first workbench (dodo · ngin)      │\n│   contributions · commands · keymaps · settings · plugin host  │\n├──────────────────────────────────────────────────────────────┤\n│  @3sln/trove/server  Request → Response  (Node · Worker)       │\n├──────────────────────────────────────────────────────────────┤\n│  @3sln/trove/core  Vfs · Storage · Metadata · Uploads · Search │\n│   S3 / filesystem / NAS   ·   SQLite / memory   ·   embeddings │\n└──────────────────────────────────────────────────────────────┘\n```\n\n## Highlights\n\n- **Pluggable storage** — S3-compatible (AWS S3, Cloudflare R2, MinIO, B2),\n  local filesystem, or a NAS mount. S3 uses **presigned URLs** so large uploads\n  and downloads go **straight to the bucket**, never proxied through the server.\n  SigV4 is implemented on Web Crypto, so it works on Workers too — no AWS SDK.\n- **Resumable large transfers** — multipart uploads with bounded concurrency,\n  per-part retry, live progress, and resume-after-drop. Range-aware downloads\n  (media seeking, partial fetch).\n- **Semantic + keyword search** — a hybrid `SearchService` blends dense vector\n  similarity with lexical matching. **Every piece is a pluggable, async provider\n  you inject into the server constructor**: the embeddings (offline hash model or\n  any OpenAI-compatible endpoint), the **vector store** (in-memory brute-force by\n  default, or an external DB — a Qdrant adapter ships in core, and the\n  `VectorStore` interface fits pgvector/Pinecone/Milvus/LanceDB), and the keyword\n  store. Core hardcodes none of them and stays platform-agnostic.\n- **Pluggable indexers** — attach searchable content to files, namespaced under\n  the indexer that owns it. A built-in text/code extractor runs server-side;\n  plugins push their own documents through the API under their namespace.\n- **No folders** — a collection is a **flat set of uniquely-named items**. You find\n  things by searching, and you group them by **linking**: any item is addressable as\n  `trove:<collection>?name=…` (or `?id=…`), so a markdown document that links its\n  sources does what a folder did — except it can say *why* those things belong\n  together, an item can appear in as many documents as you like, and the grouping is\n  searchable content rather than an invisible box. A links indexer records those\n  references, so every item shows **what links to it**.\n- **Search-first workbench** — the main panel is a launcher (Spotlight/Raycast\n  style): type to search files, `!` to run a command, `#tag` / `#key:>=value` to\n  filter by tag or property; recents and the collection sit underneath. Opening\n  a file shows the opener **beside** the launcher (split) or **over** it (modal) —\n  your last choice is the default, and you can swap. Underneath is a real\n  contribution system: commands, a command palette + quick-open, keybindings\n  (chords, user overrides), when-clauses, schema-driven settings, and media openers.\n- **Media openers** — markdown (with live `trove:` links), text, image, audio and\n  video. Deliberately plain: an opener is a contribution, so a richer player is\n  something a build or a plugin adds rather than something this one has to guess at.\n- **Views** — how the *results* are drawn is a contribution too. A list and a grid\n  ship; the switcher sits in the search box and remembers your choice, and a\n  collection that is mostly photographs opens as a grid without being asked. A gallery,\n  a map or a table is a `view` contribution, not a patch to the launcher — the\n  launcher still owns the items, the highlight and the keyboard, so every view\n  navigates the same way. The **search transformer can suggest one** — it is the only\n  thing in the stack that read the sentence, and \"photos from the trip last summer\"\n  asks for a gallery as much as it asks for files. It names a view the client offered,\n  and a view you picked yourself still wins.\n- **Build your own drive** — `createWorkbench({ openers, views })` is the entry point,\n  so a bespoke or hosted build ships its own first-party openers and views through the\n  same registry plugins use. No fork of `@3sln/trove/web`.\n- **Sandboxed plugins** — plugins are **self-contained ZIP packages** (a\n  `manifest.json`, an entry script, and any assets) installed by **URL or file\n  upload** — no central catalogue. Each runs in a **hidden, sandboxed iframe on an\n  opaque origin** (`allow-scripts`, no `allow-same-origin`): it can't touch the\n  host DOM, cookies, or storage, and can't even fetch its own package files. The\n  host injects the SDK + the plugin's entry script into the frame and hands it a\n  single `MessagePort`; **package resources arrive as opaque byte handles** over\n  that port. Everything a plugin can do — file access, storage, UI — is gated by\n  the **capabilities the user grants at install time**. A plugin has **no direct network\n  access** — the sandbox blocks all egress (`connect-src 'none'`); to reach the\n  web it must **declare each endpoint** in its manifest, and the host brokers every\n  request, refusing anything off the declared allowlist (including redirects) and\n  sending no ambient cookies. Before anything runs, a\n  **pre-install review** shows the package's identity, capabilities (each\n  explained), contributions, and settings so the user can decide whether to trust\n  it. Signed packages show a **domain-verified** badge: the manifest declares a\n  domain, and the host checks the signing key's fingerprint against an\n  `assetlinks`-style document published at that domain (Digital Asset Links\n  style). Plugins get **persistent SQLite storage** — an isolated database per\n  scope, both **server-side** (native SQLite via a keyed provider) and **on-device**\n  (sql.js/wasm run in the host, persisted to IndexedDB) behind one async SQL\n  interface. Scopes are `plugin` (private) and `domain` (shared across a vendor's\n  plugins — **verified packages only**); Trove **tracks which plugin owns what\n  data** so uninstalling wipes it. They contribute commands, openers, indexers,\n  status items, and keybindings, and can surface a popup UI panel. Plugins\n  **announce a live capability manifest** on connect (and re-announce when the app\n  goes on/offline), each contribution flagged offline-capable or not — so the\n  workbench knows which plugin features work right now, disables the ones that\n  don't (e.g. a network-only previewer while offline), and treats a plugin that\n  sends no manifest as not running. The host also **re-requests the manifest on a\n  heartbeat**, so a plugin that hangs or crashes between events is noticed and its\n  features are marked unavailable.\n- **Conversations on every file** — threaded comments with @mentions, reactions,\n  and tags, stored in a **CRDT sidecar document** kept cold in object storage\n  (one `sidecars/<id>.json` next to your data — no extra database) and loaded\n  into a hot, debounced, merge-on-write manager when active. Indexer facets live\n  there too, scoped to the indexer that wrote them.\n- **Bring-your-own identity** — Trove ships no login. It verifies an identity JWT\n  (Cloudflare Access / Zero Trust, oauth2-proxy, any IdP) via JWKS/RS256/ES256 —\n  or a proxy-set header — on Web Crypto, and builds a profile from the claims. You\n  can also name the keys you trust directly (`TROVE_JWT_JWKS`), so a deployment that\n  mints its own tokens needs no JWKS endpoint. With no identity configured at all\n  there is one shared anonymous user and **no profile is shown** — an avatar for\n  somebody who doesn't exist implies an account there is no way to sign in to.\n- **Mention notifications over Web Push** — as conversations change, @mentions\n  batch per user and flush on an interval as **bodyless VAPID web pushes**; the\n  service worker wakes and pulls the inbox. No mention text ever touches a\n  third-party push service.\n- **Cloudflare-native** — the vector store speaks **Vectorize** (binding or REST),\n  storage speaks **R2**, and identity speaks **Access** — first-class, env-driven.\n- **Collections** — every item belongs to a collection, which is both a permission\n  boundary (read / write / delete / admin grants by user, role, or anyone) and a\n  **store config**: each collection points at its own backend (an S3 bucket+prefix,\n  a filesystem path, …). Users with the create capability provision new\n  collections dynamically by configuring the backing store.\n- **Offline mode (PWA)** — a service worker caches the app shell and every\n  built-in media player/previewer; \"make available offline\" pins a file's bytes\n  (served from cache) and indexes its text for **offline hybrid search**\n  (lexical + local vectors). Comments and tags written offline are **queued and\n  merged** (the sidecar is a CRDT) when you reconnect.\n\n## Quick start\n\n```sh\nnpm install\n\n# 1. Run the API + web app together, in-memory (zero config):\nnpm run build:web        # builds with Bun\nnpm run serve            # Bun runtime → http://localhost:8787\n#   (npm run serve:node  # same server under Node ≥22.5, if you prefer)\n\n# — or, for development with hot reload —\nnpm run serve &          # API on :8787\nnpm run dev              # @web/dev-server on :5173 (unbundled ESM + HMR, proxies /api to :8787)\n```\n\nThen open the app, drag files in, and try the command palette (`⌘/Ctrl‑Shift‑P`)\nor semantic search (`⌘/Ctrl‑Shift‑F`).\n\n### Configure the backends (env)\n\n```sh\n# Storage\nTROVE_STORAGE=filesystem            # memory | filesystem | s3\nTROVE_FS_ROOT=./data/objects        # for filesystem/NAS (point at a mount)\n# …or S3 / R2 / MinIO:\nTROVE_STORAGE=s3\nTROVE_S3_BUCKET=my-bucket\nTROVE_S3_REGION=auto\nTROVE_S3_ENDPOINT=https://<acct>.r2.cloudflarestorage.com   # omit for AWS\nTROVE_S3_ACCESS_KEY_ID=…            # or AWS_ACCESS_KEY_ID\nTROVE_S3_SECRET_ACCESS_KEY=…        # or AWS_SECRET_ACCESS_KEY\nTROVE_S3_PATH_STYLE=true            # MinIO / custom endpoints\n\n# Which store types a COLLECTION may be created on. Defaults to everything this\n# runtime registered; naming a subset takes the rest off the collection form and\n# refuses them. Worth setting on Workers, where `memory` is offered because it is\n# portable but produces a collection that loses its uploads on isolate recycle.\nTROVE_STORAGE_DRIVERS=s3            # subset of: memory | filesystem | s3\n\n# Metadata (file tree + facets)\nTROVE_METADATA=sqlite               # memory | sqlite\nTROVE_DB_PATH=./data/trove.db\n\n# Semantic search embeddings (optional — defaults to an offline local model)\nTROVE_EMBEDDINGS_URL=https://api.openai.com/v1/embeddings\nTROVE_EMBEDDINGS_API_KEY=sk-…\nTROVE_EMBEDDINGS_MODEL=text-embedding-3-small\nTROVE_EMBEDDINGS_DIM=1536\n```\n\n> **S3 CORS:** for browser-direct presigned uploads/downloads, allow `PUT`/`GET`\n> and expose the `ETag` header on your bucket's CORS policy.\n\n## Deploy\n\nThe server is one function — `handle(Request) -> Promise<Response>` — so an adapter is\nthin and there is no runtime-specific code below it. Pick a row:\n\n| runtime | storage | metadata + search | notes |\n| --- | --- | --- | --- |\n| **Bun** | filesystem / S3 | SQLite file | recommended for self-hosting |\n| **Node** | filesystem / S3 | SQLite file | identical behaviour, a little slower |\n| **Workers** | R2 (S3 API) | D1 + Vectorize | no local disk, so both must be bound |\n\n### Scaffold one\n\n```sh\nnpm create @3sln/trove my-drive\n```\n\nAsks where the drive will run — Bun, Node or Cloudflare Workers — and writes a project\nconfigured for it: storage, metadata, search, identity, access control, and on Workers\nthe whole binding set (D1, Vectorize, R2, the `TroveTasks` Durable Object) plus the\n`wrangler` commands that create each of them. Any section can be declined, and declining\nstill writes the keys, commented, with a line saying what they are for.\n\nCredentials never land in a committed file: on Workers they become `wrangler secret put`\nsteps and a gitignored `.dev.vars`; everywhere else a gitignored `.env`.\n\n#### Without a person\n\nEvery question has a stable key, so the whole thing can be driven by a script or an\nagent. Keys are a flat namespace rather than the text of a question — rewording a hint\nshould not break a caller.\n\n```sh\nnpm create @3sln/trove -- --describe        # every key, its type and its default\n\nnpm create @3sln/trove drive -- --runtime=workers --json \\\n  --set storage.bucket=acme-objects \\\n  --set identity.driver=cloudflare-access --set identity.team=acme \\\n  --set workers.d1.id=db-abc\n```\n\n`--json` puts one parseable object on stdout — files written, commands to run next,\nwarnings, and anything skipped — with every human word on stderr. `--config file.json`\ntakes the same keys in bulk and `--set` overrides it. `--dry-run` answers \"what would\nyou do\" without writing.\n\nAn answer that is never asked for is an **error**, not a shrug: it means either a typo or\na setting another answer ruled out (`storage.bucket` when the backend is `filesystem`),\nand a caller that thinks it configured a bucket should not get a drive without one.\n\nThe same thing is available as a library, which is what `--describe` is generated from:\n\n```js\nimport { createProject, describeQuestions } from '@3sln/create-trove';\n\nconst { files, steps, unused } = await createProject({\n  name: 'drive', version: '0.0.3', runtime: 'node',\n  answers: { 'storage.root': '/srv/objects', 'identity.driver': 'header' },\n});\n```\n\n### From npm\n\nTrove publishes as one package with the web app already built inside it, so there is no\nbuild step here — the server and the workbench it serves are the same release by\nconstruction.\n\n```sh\nnpm install @3sln/trove\nTROVE_STORAGE=filesystem TROVE_FS_ROOT=./data/objects \\\nTROVE_METADATA=sqlite TROVE_DB_PATH=./data/trove.db \\\nnode node_modules/@3sln/trove/packages/server/src/adapters/node.js\n```\n\nBun works the same way — swap `node` for `bun` and `node.js` for `bun.js`. Building is\nonly for working *on* Trove, which is what the rest of this section covers.\n\n### Bun (recommended)\n\n```sh\nbun install\nbun run build:web                       # builds packages/web/dist\nTROVE_STORAGE=filesystem TROVE_FS_ROOT=./data/objects \\\nTROVE_METADATA=sqlite TROVE_DB_PATH=./data/trove.db \\\nbun packages/server/src/adapters/bun.js  # :8787, API + web app\n```\n\nThat is the whole thing: object bytes under `$TROVE_FS_ROOT/objects/` (the backend\ncreates that subdirectory, sharded two levels deep), everything else in one SQLite file. Back it up with `npm run backup` (a `VACUUM INTO` snapshot, safe on a live\ndatabase) and copy the objects directory.\n\n### Node\n\nIdentical, with `node`:\n\n```sh\nTROVE_STORAGE=filesystem TROVE_FS_ROOT=./data/objects \\\nTROVE_METADATA=sqlite TROVE_DB_PATH=./data/trove.db \\\nnode packages/server/src/adapters/node.js\n```\n\nNode 20+ for `node:sqlite`. Both adapters serve the built web app with SPA fallback and\ntrap `SIGTERM`/`SIGINT` to shut down cleanly — flush notifications, stop an in-flight\nreindex, close SQLite — so a redeploy doesn't lose work. See [`Dockerfile`](./Dockerfile).\n\n### Cloudflare Workers\n\nThere is no disk, so the two things a self-hosted run keeps in a file need bindings:\n\n```toml\n# wrangler.toml\nmain = \"packages/server/src/adapters/worker.js\"\ncompatibility_date = \"2024-09-23\"\n\n[[d1_databases]]                 # metadata, KV, plugin installs, keyword search\nbinding = \"DB\"\ndatabase_name = \"trove\"\ndatabase_id = \"...\"\n\n[[vectorize]]                    # semantic search (sqlite-vec cannot run here)\nbinding = \"VECTORIZE\"\nindex_name = \"trove\"\n\n[ai]                             # optional: LLM query understanding\nbinding = \"AI\"\n\n[assets]                         # the built web app\ndirectory = \"packages/web/dist\"\nbinding = \"ASSETS\"\n\n[[durable_objects.bindings]]     # owns scans and reindexes — see below\nname = \"TASKS\"\nclass_name = \"TroveTasks\"\n\n[[migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"TroveTasks\"]\n\n[vars]\nTROVE_STORAGE = \"s3\"             # R2 through the S3 API\nTROVE_S3_BUCKET = \"trove\"\nTROVE_S3_REGION = \"auto\"\nTROVE_S3_ENDPOINT = \"https://<account>.r2.cloudflarestorage.com\"\nTROVE_AUTH = \"cloudflare-access\"\nTROVE_CF_ACCESS_TEAM = \"acme\"\nTROVE_CF_ACCESS_AUD = \"<aud-tag>\"\nTROVE_ADMINS = \"you@example.com\"  # see \"Making yourself an admin\" below\nTROVE_DEFAULT_OPEN = \"false\"      # otherwise every Access user gets the default collection\n```\n\n```sh\nwrangler secret put TROVE_S3_ACCESS_KEY_ID\nwrangler secret put TROVE_S3_SECRET_ACCESS_KEY\nwrangler d1 execute trove --command \"SELECT 1\"   # create it first\nwrangler deploy\n```\n\nThe adapter wires `DB` through `D1SqliteProvider` and `VECTORIZE` through\n`VectorizeVectorStore` on its own. **Bind `DB` or the drive runs entirely in memory** —\nwhich works right up until the isolate is recycled and everything is gone. R2 works\nthrough the S3 API rather than the R2 binding because that is what makes presigned\nuploads go straight to the bucket instead of through your Worker's CPU time.\n\nTwo Workers-specific limits worth knowing before you commit: `sqlite-vec` is a native\nartifact and cannot load, so semantic search *needs* Vectorize; and plugin scopes each\nwant their own D1 database, since D1 cannot create one on demand and co-locating them\nwould put a plugin's tables next to the drive's metadata. Bind `PLUGIN_DB` if you use\nserver-side plugin storage — without it, that one feature reports a clear error and the\nrest of the drive is unaffected. One D1 database holds every plugin scope: D1 cannot\ncreate databases on demand and a scope key contains the user's id, so per-scope\ndatabases are not expressible here. Their tables sit side by side, which is weaker\nisolation than the file-per-scope a self-hosted run gets.\n\n#### Plugin storage\n\nA plugin with the `storage` capability gets an isolated SQLite database per scope — its\nown, and optionally one shared with the rest of its vendor's plugins. The scope key\nembeds the user *and* the plugin (`pstore:<principal>:plg:<pluginId>`), so it can never be\npre-bound: **D1 cannot create a database on demand.**\n\nSo a Worker deployment has two options, and they are not equivalent.\n\n**A Durable Object per scope** — bind `PLUGIN_STORE` to the `TrovePluginStore` class. A\nDurable Object is addressable by name, so each `(user, plugin)` becomes its own object with\nits own SQLite database, created on first use. The isolation is structural.\n\n```toml\n[[durable_objects.bindings]]\nname = \"PLUGIN_STORE\"\nclass_name = \"TrovePluginStore\"\n\n[[migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"TroveTasks\", \"TrovePluginStore\"]\n```\n\nand export it beside the fetch handler:\n\n```js\nexport { default, TroveTasks, TrovePluginStore } from '@3sln/trove/server/adapters/worker.js';\n```\n\n**One shared D1** — bind `PLUGIN_DB` and every plugin's tables for every user live in it\nside by side. The keys stay distinct; the boundary is a naming convention rather than a\nwall. It works, and it is what you get without the Durable Object, and it is worth knowing\nwhich of the two you have.\n\nCore stores — metadata, KV, install records, the keyword index — stay on D1 either way.\nThey are one per deployment, so they can simply be bound, and routing the whole drive\nthrough one single-threaded object would be a bottleneck rather than an isolation win.\n\n#### Work that outlives a request\n\nA Worker isolate is not a server: it may be discarded as soon as the response resolves,\nso a promise nobody declared is simply cancelled part-way through. That matters here\nbecause the drive has work that intentionally outlives the request that started it — a\nscan or a reindex takes minutes, and `POST /api/collections/:id/scan` returns a task\nrecord immediately rather than holding the connection open for it. On Node and Bun the\nprocess keeps that promise alive. On Workers, without help, you would get a scan that\ndid a third of the bucket and reported success.\n\nThree mechanisms cover it. Two need a line in your config.\n\n**`ctx.waitUntil`** — the adapter hands the runtime every task still running when the\nresponse is ready, so the isolate stays alive until they finish. Automatic, no config.\nIt buys a bigger bite, not an unlimited one: CPU is still capped per invocation.\n\n**A Durable Object** — this is the one that matters, and the reason is worth stating.\n`waitUntil` can keep work alive, but it cannot let a *different* isolate see it. The\nscan runs wherever the POST landed; the GET that polls it lands wherever the router\nfeels like; Cancel lands somewhere else again. Bind the object and all three reach the\nsame place:\n\n```toml\n[[durable_objects.bindings]]\nname = \"TASKS\"\nclass_name = \"TroveTasks\"\n\n[[migrations]]\ntag = \"v1\"\nnew_sqlite_classes = [\"TroveTasks\"]\n```\n\nWith `TASKS` bound, scans and reindexes run *inside* the object, and it becomes the one\nplace their tasks are listed. Progress polling shows real progress, Cancel reaches the\nwork it means to abort, and \"is one already running?\" is answered by a single instance\nrather than by whichever isolate happened to be asked. It also keeps itself moving with\n`setAlarm`, so a bucket too large for one slice continues in ~5 s rather than waiting\nfor the next cron tick.\n\nNote what did **not** change: the task list is still in memory and still per-process.\nMaking it durable would be the wrong fix — a stored record saying `running` after the\nisolate that owned it was evicted is a phantom nothing can ever correct, where a\nrestart clears an in-memory one. The registry didn't need to become durable; the work\nneeded a real process to live in. Failures are already durable, as Issues.\n\n**Cron Triggers** — `setInterval` does not survive the request it was registered in, so\n`TROVE_SCAN_INTERVAL_MS` and `TROVE_MAINTENANCE_INTERVAL_MS` do nothing here; they are\nNode/Bun only. Periodic work runs from the `scheduled` handler instead:\n\n```toml\n[triggers]\ncrons = [\"*/5 * * * *\"]\n\n[vars]\nTROVE_CRON_BUDGET_MS = \"20000\"   # wall-clock a cron firing may spend; default 20s\nTROVE_SLICE_MS = \"20000\"         # wall-clock one Durable Object slice may spend\n```\n\nEach firing sweeps abandoned uploads and unflushed sidecars, applies trash retention,\nthen scans — splitting whatever budget is left across your collections. A scan that\nruns out of budget **stores the cursor it reached** and stops, so the next slice\ncontinues from there rather than restarting. Keep the budget under your Worker's CPU\nlimit (30 s by default) with room to spare.\n\nWithout the `TASKS` binding the drive still works — background work runs in the request\nisolate under `waitUntil`, exactly as it did before. What you lose is visibility: a\nclient polling `/api/tasks` may reach an isolate that never saw the scan and show\nnothing running, and Cancel on such a task silently does nothing.\n\nIndependently of any of this, a scan **claims its collection** before it starts, through\na lease in the metadata database (`KeyValueStore.acquire`). Two scans of one collection\nrunning at once would both write the resume cursor, last-writer-wins, and a slice of the\nbucket would be silently skipped — so the guard has to live where every process can see\nit, not in one process's memory. That applies to multi-instance Node and Bun deployments\ntoo, not just Workers.\n\n### Making yourself an admin\n\nAn admin can install plugins that ship server code, create collections, and rebuild the\nsearch index — the operations that are drive-wide rather than per-collection.\n\n```toml\n[vars]\nTROVE_ADMINS = \"you@example.com,ops@example.com\"\n```\n\nThe list is matched against each request's principal **by email or by id**, so the\naddress you sign in to Access with is the thing to write down. This matters more than it\nsounds: Cloudflare Access puts an internal user UUID in the token's `sub` claim, which\nis what becomes `principal.id` — so an admin list of ids would mean pasting\n`8f2a1c04-6d3e-…` and having no way to find it short of decoding a JWT.\n\nCheck it worked. `admin` is the whole answer:\n\n```sh\ncurl -s https://drive.example.com/api/me | jq\n# { \"principal\": { \"id\": \"8f2a1c04-…\", \"email\": \"you@example.com\", … },\n#   \"authenticated\": true, \"admin\": true }\n```\n\n`authenticated: false` means Access isn't reaching the origin — the Worker is being\ncalled directly, or the Access application doesn't cover this hostname. `admin: false`\nwith the right email means the address in `TROVE_ADMINS` doesn't match the one in the\ntoken; `/api/me` shows you both.\n\nTwo things worth doing at the same time:\n\n- **`TROVE_DEFAULT_OPEN=false`.** The default collection is open to anyone who reaches\n  it, which is right for a single-user drive and wrong the moment Access lets a team in.\n  With it off, access comes only from an explicit grant — and the admin list is one.\n- **Share by email too.** A per-collection grant (`{ type: \"user\", subject: … }`) matches\n  the same way, so an ACL written by a human names people the way humans do.\n\nThe whole Access setup, including what to put in the Zero Trust dashboard, is in\n[Cloudflare Access (Zero Trust)](#cloudflare-access-zero-trust).\n\n### Every setting\n\nDefaults are what you get with the variable unset. Everything here is read once at\nstartup by `configFromEnv` (`packages/server/src/index.js`), so a library caller can pass\nthe same values as config fields instead.\n\n| variable | default | what it does |\n| --- | --- | --- |\n| **storage** | | |\n| `TROVE_STORAGE` | `memory` | `memory` · `filesystem` · `s3` |\n| `TROVE_FS_ROOT` | `./data/objects` | filesystem root; bytes go in `<root>/objects/` |\n| `TROVE_S3_BUCKET` / `_REGION` / `_ENDPOINT` | — | S3/R2/MinIO; endpoint for non-AWS |\n| `TROVE_S3_ACCESS_KEY_ID` / `_SECRET_ACCESS_KEY` | — | credentials (use secrets, not env files) |\n| `TROVE_S3_PATH_STYLE` | `false` | MinIO and most S3-compatibles need `true` |\n| `TROVE_MAX_UPLOAD_BYTES` | unlimited | per-file ceiling; over it is `413`, not `507` |\n| **metadata + search** | | |\n| `TROVE_METADATA` | `sqlite` unless storage is memory | `memory` · `sqlite` |\n| `TROVE_DB_PATH` | `./data/trove.db` | the one file holding metadata, KV, and installs |\n| `TROVE_VECTOR` | follows durability | `sqlite` · `memory` · `qdrant` · `vectorize` |\n| `TROVE_KEYWORD` | follows durability | `sqlite` · `memory` |\n| `TROVE_EMBEDDINGS_URL` / `_KEY` / `_MODEL` / `_DIM` | offline hash model | any OpenAI-compatible endpoint |\n| `TROVE_SEARCH_TRANSFORMER` | `parse` | `parse` · `workers-ai` (query understanding) |\n| `TROVE_REBUILD_INDEX_ON_START` | `true` | rebuild when the index is empty and the drive isn't |\n| **identity** | | |\n| `TROVE_AUTH` | `anonymous` | `anonymous` · `cloudflare-access` · `jwt` · `header` |\n| `TROVE_AUTH_REQUIRED` | `false` | **set this** — else unauthenticated is anonymous |\n| `TROVE_CF_ACCESS_TEAM` / `_AUD` | — | Cloudflare Access, derives everything else |\n| `TROVE_JWKS_URL` / `TROVE_JWT_JWKS` / `_JWKS_FILE` | — | keys to trust: fetched, inline, or a file |\n| `TROVE_JWT_ISSUER` / `_AUDIENCE` / `_ALGS` | — | claim checks after the signature passes |\n| `TROVE_AUTH_SERVER` | the JWT issuer | where refused clients are sent to sign in |\n| `TROVE_ADMINS` | — | comma-separated ids with whole-drive rights |\n| **rate limits** | | |\n| `TROVE_RATE_LIMIT` | on | `off` to meter nothing |\n| `TROVE_RATE_LIMIT_STORE` | `memory` | `kv` for one budget across instances (needed on Workers) |\n| `TROVE_RATE_LIMITS` | — | JSON per class, e.g. `{\"search\":{\"limit\":20,\"windowMs\":60000}}` |\n| **collections** | | |\n| `TROVE_DEFAULT_OPEN` | `true` | **set `false`** before exposing it |\n| `TROVE_COLLECTION_CREATOR_ROLES` | — | roles allowed to create collections |\n| **agents** | | |\n| `TROVE_MCP` | on | `off` disables the endpoint |\n| `TROVE_MCP_PATH` / `_RESOURCE` / `_REQUIRE_AUTH` | `/mcp` | see [Connecting an AI agent](#connecting-an-ai-agent-mcp) |\n| **housekeeping** | | |\n| `TROVE_TRASH_DAYS` | `30` | `0` keeps the trash forever |\n| `TROVE_SCAN_INTERVAL_MS` | off | reconcile with the store on a timer (not Workers) |\n| `TROVE_MAINTENANCE_INTERVAL_MS` | `300000` | sweep stale uploads and sidecars (not Workers) |\n| `TROVE_CRON_BUDGET_MS` | `20000` | Workers only: wall-clock one cron firing may spend |\n| `TROVE_SLICE_MS` | `20000` | Workers only: wall-clock one Durable Object slice may spend |\n| `TROVE_MENTION_FLUSH_MS` | — | how often mention notifications batch out |\n| `TROVE_VAPID_PUBLIC_KEY` / `_PRIVATE_KEY` / `_SUBJECT` | — | Web Push for @mentions |\n| **limits + serving** | | |\n| `TROVE_MAX_JSON_BYTES` | `4 MiB` | JSON body cap (uploads stream, so bound those at the proxy) |\n| `TROVE_MAX_PAGE` | `1000` | ceiling on any client-supplied `limit` |\n| `TROVE_PORT` / `TROVE_HOST` | `8787` / `0.0.0.0` | |\n| `TROVE_WEB_DIST` | resolved from the package | built web app to serve; unset serves API only |\n| `TROVE_CORS_ORIGIN` | off | `*` or an allowlist; the app is same-origin (MCP follows it too) |\n| `TROVE_PUBLIC_URL` | detected | the drive's public origin, for sign-in discovery |\n| `TROVE_TRUST_PROXY` | `false` | honour `X-Forwarded-Proto/Host` — only behind a real proxy |\n| `TROVE_CSP` | off | opt-in shell CSP (see `SAMPLE_CSP`) |\n| **installed app** | | served at `/manifest.webmanifest`, generated from these |\n| `TROVE_APP_NAME` | `Trove` | what the installed app is called |\n| `TROVE_APP_SHORT_NAME` | the app name | home-screen label |\n| `TROVE_APP_DESCRIPTION` | the stock one | |\n| `TROVE_APP_THEME_COLOR` | `#181a1f` | |\n| `TROVE_APP_BACKGROUND_COLOR` | the theme colour | splash background |\n| `TROVE_APP_DISPLAY` | `standalone` | `fullscreen`, `minimal-ui`, `browser` |\n| `TROVE_APP_START_URL` | `/` | |\n| `TROVE_APP_ICON` | `/icon.svg` | any URL the browser can reach |\n| `TROVE_APP_ICON_SIZES` | `any` | state the real size for a raster icon |\n| `TROVE_APP_ICONS` | — | the full icon array, for maskable or multi-size sets |\n| **plugins** | | |\n| `TROVE_SERVER_INDEXERS` | on | `false` disables server-side plugin indexers |\n| `TROVE_ENFORCE_PLUGIN_CAPS` | `false` | strict capability enforcement |\n| `TROVE_PACKAGE_STORE` / `TROVE_PACKAGE_FS_ROOT` | primary storage | where plugin zips live |\n\n### Before you expose it\n\nTrove ships **no login**, and a zero-config run is **open to anyone who can reach\nthe port** (anonymous auth + an open default collection — you'll see a startup\nwarning). Before putting it on a network:\n\n- **Authenticate.** Set `TROVE_AUTH=jwt` (verify a JWT via `TROVE_JWKS_URL`, e.g.\n  Cloudflare Access) or `TROVE_AUTH=header` (trust a header a verifying proxy\n  set), plus `TROVE_AUTH_REQUIRED=true` so unauthenticated requests are rejected\n  rather than treated as anonymous. Consider `TROVE_DEFAULT_OPEN=false` and\n  `TROVE_ADMINS=…`.\n- **Terminate TLS at a reverse proxy** (Caddy, nginx, Traefik, Cloudflare) — the\n  server itself speaks plaintext on `0.0.0.0:8787`. Front it with the proxy and\n  don't publish the port directly. Also cap the proxy's max request body size —\n  the server caps JSON bodies (`TROVE_MAX_JSON_BYTES`) but streams file-upload\n  parts straight to storage, so bound raw upload size at the proxy (and/or set\n  disk/bucket quotas) to prevent a write-capable user from filling the store.\n- **Set `TROVE_PUBLIC_URL`** to the address people actually reach the drive at. It is\n  what the sign-in challenge and the agent discovery document advertise. Trove will\n  otherwise read it off the request, and `X-Forwarded-Host` is set by whoever is talking\n  to it — so that header is honoured only with `TROVE_TRUST_PROXY=true`, which is safe\n  exactly when a proxy is guaranteed to be in front.\n- **CORS stays off** unless you set `TROVE_CORS_ORIGIN` (the app is same-origin).\n  A shell **CSP** is opt-in via `TROVE_CSP` (see `SAMPLE_CSP`); it's off by\n  default because sandboxed plugin iframes can't satisfy a strict one. The API\n  still forces attachment downloads + `nosniff` to neutralize uploaded HTML/SVG.\n- **Cross-site writes are refused**, independently of CORS. An allowlist only decides\n  who may *read* a reply, and only for requests a browser preflights — a `POST` with\n  `content-type: text/plain` is a CORS *simple* request, so it is sent without one and\n  the deletion happens whether or not anyone can read the answer. So every\n  state-changing request (JSON API and MCP alike) is checked against `Sec-Fetch-Site`\n  and `Origin`, and a cross-site one gets a 403. Non-browser clients — curl, an agent,\n  a script — send neither header and are unaffected; they carry no ambient credential\n  for another site to borrow, which is the whole basis of the attack. Setting\n  `TROVE_CORS_ORIGIN` to a specific origin permits that origin to write, too.\n\n### Naming the keys you trust\n\nThree ways to say who a request is from, in the order most deployments reach for\nthem:\n\n```sh\n# 1. A proxy already authenticated the user and set a header (Cloudflare Access,\n#    oauth2-proxy). The browser sends nothing; Trove trusts the header.\nTROVE_AUTH=header TROVE_AUTH_ID_HEADER=cf-access-authenticated-user-email\n\n# 2. Cloudflare Access / Zero Trust — the team name is the only thing that isn't\n#    derivable. TROVE_CF_ACCESS_AUD is the Access *application's* AUD tag: without it,\n#    a token minted for any other app in the same Access account verifies here too.\nTROVE_AUTH=cloudflare-access TROVE_CF_ACCESS_TEAM=acme TROVE_CF_ACCESS_AUD=<aud-tag>\n\n# 3. Any other IdP that publishes a JWKS you fetch.\nTROVE_AUTH=jwt TROVE_JWKS_URL=https://issuer.example.com/.well-known/jwks.json \\\nTROVE_JWT_ISSUER=https://issuer.example.com TROVE_JWT_AUDIENCE=trove\n\n# 4. You mint your own tokens, so there is no JWKS endpoint to point at — name the\n#    keys directly. Inline JSON, or a file (which keeps a multi-line document out of\n#    the environment and out of `docker inspect`).\nTROVE_AUTH=jwt TROVE_JWT_JWKS_FILE=/run/secrets/trove-jwks.json \\\nTROVE_JWT_ISSUER=https://you.example TROVE_JWT_AUDIENCE=trove\n```\n\n**What is trusting what.** The signature is verified against the key material, and\nnothing else: `TROVE_JWT_JWKS` is a key set you hold (nothing is fetched, so there is\nnothing to spoof), `TROVE_JWKS_URL` is one Trove fetches over HTTPS (so the authenticity\nof those keys rests on that host's TLS certificate), and `TROVE_JWT_SECRET` is a shared\nHS256 secret. The key is chosen by the token's `kid`.\n\n`TROVE_JWT_ISSUER` and `TROVE_JWT_AUDIENCE` are **claim checks**, not trust anchors — a\nstring comparison against `iss` and `aud` after the signature has already passed. They\ncost nothing and are worth setting: they stop a token that is validly signed by a key you\ntrust but was minted for a different issuer or a different application, which is a real\ncase when a JWKS serves several or the IdP is multi-tenant. On their own they secure\nnothing, since anyone forging a token also sets those claims.\n\nAlways add `TROVE_AUTH_REQUIRED=true` so an unauthenticated request is rejected\nrather than treated as anonymous. `TROVE_JWT_ALGS` narrows the accepted algorithms\n(the default is inferred from the key material: `HS256` for a secret, `RS256`/`ES256`\nfor a key set). A key set with more than one key requires a `kid` on the token —\ntrying each key until one verifies would turn key rotation into key confusion.\n\nThe **web client** presents a bearer token from `localStorage['trove.token']` when\none is present. It isn't needed for the proxy-authenticated case (the browser's\nexisting session covers it), and note that with a bearer token, downloads are\nfetched and handed to the browser as a blob rather than streamed — an `<a href>`\ncan't carry an Authorization header, and putting the token in the URL would leak it\ninto logs and history.\n\n### Running out of room\n\nA filesystem or NAS collection reports how much space is left — a gauge in the status\nbar, amber under 10% free and red under 5%. An S3 collection shows nothing at all,\nbecause an object store has no such number and a made-up meter is worse than none.\n\nWhen the disk does fill, the failure is specific rather than generic: **507\nInsufficient Storage**, not retryable, with a message that says what happened. (429\nwould tell the client to back off and try again — which against a full disk is an\ninfinite loop, since only a human can clear it.) The condition is also recorded as a\nstanding issue, so the person who needs to fix it hears about it even if they weren't\nthe one whose upload failed. Reads, search and downloads keep working throughout.\n\n### Deleting\n\nDeleting moves an item to the **trash**: it leaves the drive — gone from listings,\nsearch, name lookups and backlinks — but the bytes stay exactly where they are and\nthe record keeps its id. A confirm dialog is not a safety net; it is a thing people\nclick through, and on a drive holding your only copy of something that matters.\n\n```\nPOST /api/items/delete       → trash it (recoverable)\nGET  /api/trash              → what's in there\nPOST /api/trash/restore      → put it back, re-indexed\nPOST /api/trash/purge        → destroy one item, or empty the trash\n```\n\nRestoring re-indexes the item, so it is findable again rather than merely visible.\nIf its name was taken while it was away, it comes back under a free one — someone\nrestoring a file wants the file, not an error about a name.\n\n`TROVE_TRASH_DAYS` (default 30) is how long an item stays recoverable; `0` keeps\nthe trash forever. That timer is the only thing in Trove that destroys data\nwithout someone asking, which is why it is a number you set rather than a default\nburied in code.\n\n### Data & backups\n\nState lives in two places, both configurable and mounted as a volume in the\nDockerfile (`/data`):\n\n- **Objects** — `TROVE_FS_ROOT` (filesystem) or your S3/R2 bucket.\n- **Metadata + KV** — the SQLite file at `TROVE_DB_PATH` (WAL mode).\n- **The search index** — the same SQLite file: vectors via `sqlite-vec`, keywords\n  via FTS5. It is derived state, so it is not something you *have* to back up —\n  if it is missing on startup and the drive is not, Trove rebuilds it in the\n  background and says so in the log.\n\nTo back up the database safely while Trove is running:\n\n```sh\nbun scripts/backup.mjs ./data/trove.db ./backups/trove-$(date +%F).db\n```\n\n**Do not just copy the file.** In WAL mode the database is three files (`.db`,\n`-wal`, `-shm`) and your most recent writes live in the `-wal`; copying the `.db`\nalone produces a backup that opens cleanly and is silently missing them — the\nworst kind, because it looks like a backup. The script uses SQLite's `VACUUM\nINTO`, which is an online backup that takes a read lock rather than blocking\nwriters, and it refuses to overwrite an existing file.\n\n(The usual advice, `sqlite3 db \".backup out.db\"`, works too — but the `sqlite3`\nCLI is not in the image Trove ships, so inside the container it just fails.)\n\nBack up the object store separately with your storage's native tooling (`rsync`\nthe filesystem root, or bucket replication/versioning for S3/R2) — the database\nholds metadata and the search index, not your bytes.\n\nRestoring is putting both back and starting up: verified end to end on a\n3,005-item drive — same item count, same byte total, search intact, files\ndownloadable. And if the search index is ever lost on its own, Trove notices at\nstartup and rebuilds it. `/api/health` is a liveness check; `/api/ready` probes\nthe store for readiness gating.\n\n### Caching, and why a deploy doesn't strand a browser\n\nTwo kinds of URL, and only one of them is safe to keep:\n\n| | | |\n| --- | --- | --- |\n| `/assets/*` | `public, max-age=31536000, immutable` | content-addressed — the filename changes whenever the bytes do, so nothing ever needs invalidating |\n| everything else | `no-cache` | `index.html`, `sw.js`, the manifest, the icon: stable names whose contents change |\n\nGetting that backwards is the classic way to ship a blank page. `index.html` is the\nentry point, so a browser holding a cached copy goes on importing hashed modules from a\nbuild that no longer exists — a link-time failure, which kills the whole graph before a\nline runs. `no-cache` means \"revalidate\", not \"don't cache\": responses carry an `ETag`,\nso a revalidation that finds nothing changed is a 304 rather than a re-download.\n\nA miss under `/assets/` is a **404**, deliberately, rather than the SPA fallback. Any\nother answer is a stale reference to a retired build, and answering it with `index.html`\nat status 200 is what let a service worker cache HTML under a `.js` URL — permanently,\nsince a cache hit never asks the network again. The worker refuses a response whose type\ncontradicts the request as well, and its shell cache is named for the build\n(`trove-shell-<hash>`), so activating a new worker retires the old shell instead of\ninheriting it. The caches holding API responses and files pinned for offline use are\n*not* named per build — rotating those would throw away someone's offline library on\nevery deploy.\n\nIf you put a CDN or reverse proxy in front, let these headers through rather than\nreplacing them.\n\n## Background work and standing problems\n\nTwo registries, split by **lifetime** — the distinction is the design, not an\nimplementation detail:\n\n| | Tasks | Issues |\n|---|---|---|\n| What | work in flight | a problem that outlived the work |\n| Where | in memory, per process | the KV store, durable |\n| Ends when | the work ends | the underlying thing actually succeeds |\n| API | `GET /api/tasks` | `GET /api/issues` |\n\nA task that was running when the server stopped is *not* running — forgetting it is\ncorrect. But a file that failed to index is still unindexed tomorrow, so that has to\nsurvive a restart. They meet at the retry: **a failure raises an issue, retrying it\nstarts a task, and the task succeeding clears the issue.** Nothing is cleared by being\nacknowledged.\n\nThe client shows one list covering both sides of the wire — an upload running in the\nbrowser and a reindex running on the server appear together, because a user doesn't\ncare which machine is busy. Server tasks are a read-only mirror; the browser never\ndrives them. Progress is determinate (`done`/`total`/`unit`) or explicitly\nindeterminate: a caller that doesn't know the total leaves it `null` and gets a\nspinner, rather than a progress bar that guesses.\n\nTransport is adaptive polling — 1 s while something is running, a minute when idle.\nThere's no streaming transport in the server yet, and SSE through three runtime\nadapters isn't worth it to move a progress bar; only the poll would change if one\never exists.\n\n### Picking up changes made outside Trove\n\nTrove is not the only thing that can write to your bucket. Another tool, a teammate\nwith the S3 console, a sync client, a lifecycle rule — any of them leaves the drive\ndescribing a world that no longer exists, and with no folders, \"it isn't in the list\"\nis indistinguishable from \"it was never there\".\n\nA **collection scan** reconciles the two, naming the four things an object can be:\n\n| | |\n|---|---|\n| known & unchanged | nothing to do |\n| in the store only | **adopted** — an item is created, named from its key |\n| changed in place | **refreshed** — re-read and re-indexed |\n| in metadata only | **orphaned** — reported, *never* deleted automatically |\n\nThat last asymmetry is deliberate. Adopting a file is additive and reversible;\nremoving an item because a LIST call didn't mention it is neither — and listing is\nexactly the operation that fails in interesting ways (a wrong prefix, a stale\nreplica, a credential scoped elsewhere). Trove will invent an item from bytes it can\nsee. It will not destroy a record because it briefly couldn't see any.\n\n```sh\ncurl -X POST http://localhost:8787/api/collections/default/scan\n# or \"Scan Collection for Outside Changes\" in the palette\n```\n\nSet `TROVE_SCAN_INTERVAL_MS` to scan on a timer. Off by default: a scan lists every\nobject in the bucket, which costs API calls on S3 and load on a NAS. Turn it on when\nsomething other than Trove writes to the same bucket. On Workers a timer cannot outlive\na request — use a Cron Trigger instead, see\n[Work that outlives a request](#work-that-outlives-a-request).\n\n### Reindexing\n\nIndexing runs when an item is written (`writeFile`) or when an upload completes\n(`POST /api/uploads/:id/complete`) — including uploads that went straight to S3, since\nthe *complete* call is the trigger. Objects written directly into the bucket behind\nTrove's back are invisible: Trove owns its key namespace, and there's no bucket scan.\n\nA full rebuild happens automatically when the index is empty and the drive is not, and\non demand:\n\n```sh\ncurl -X POST http://localhost:8787/api/reindex     # or: \"Rebuild Search Index\" in the palette\n```\n\nIt returns a task rather than blocking. Drive-wide, so it requires either a\n`TROVE_ADMINS` admin or someone who can already read and write every collection —\nwhich the default single-user self-host is.\n\n## Where clients sign in\n\nTrove does not run a login system. It verifies tokens somebody else issued, which leaves\none question every refused request has to answer: *where do I go and get one?* A bare 401\nis a dead end for a browser and an absolute dead end for an agent, which has no human to\nask.\n\nSo the drive publishes it, once, for everything:\n\n```sh\nTROVE_AUTH_SERVER=https://auth.example.com\n```\n\nEvery 401 — from the JSON API, from the MCP endpoint — then carries a pointer:\n\n```\nWWW-Authenticate: Bearer realm=\"Trove\", error=\"invalid_token\",\n  error_description=\"...\",\n  resource_metadata=\"https://drive.example.com/.well-known/oauth-protected-resource\"\n```\n\nand that document ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728.html)) names the\nauthorization server. It is the same mechanism the MCP authorization spec is built on,\nwhich is why implementing it once serves both a browser and an agent.\n\n**You often don't have to set it.** For an OIDC provider the issuer identifier *is* the\nauthorization server — that is what RFC 8414 locates its metadata relative to — so when\n`TROVE_JWT_ISSUER` is a URL, Trove uses it. Set `TROVE_AUTH_SERVER` when they genuinely\ndiffer. Asking for the same URL under two names is a good way to end up with two\ndifferent answers.\n\nThe inference only fires when the issuer is an `https` URL (or `http` on loopback). A\nJWT `iss` is `StringOrURI`, so a deployment minting its own tokens may well have set it\nto `my-gateway` or a URN — publishing one of those as an authorization server would send\nclients off to fetch `.well-known` from a string, which fails less usefully than\npublishing nothing. In that case Trove publishes nothing and says why, at boot.\n\nNot setting either is the failure that looks like success: auth is required, and there is\nnowhere to send anyone. Trove says so in the challenge itself and in Settings, rather\nthan leaving you to infer it from an empty field.\n\nIt is deployment configuration, not a preference — pointing the drive at a different\nauthorization server changes who can reach every file in it — so it comes from the\nenvironment, or from the library caller:\n\n```js\nconst { handle } = await createServer({\n  authServer: 'https://auth.example.com',\n  identity: { driver: 'jwt', jwt: { jwksUrl: '...', audience: '...' } },\n});\n```\n\n### Cloudflare Access (Zero Trust)\n\nTwo env vars, and both the browser and agents are covered:\n\n```sh\nTROVE_AUTH=cloudflare-access\nTROVE_CF_ACCESS_TEAM=acme          # or acme.cloudflareaccess.com\nTROVE_CF_ACCESS_AUD=<aud-tag>      # the Access application's AUD\n```\n\nThat derives the JWKS URL, the issuer, and the authorization server — they are all the\nsame team domain, and writing it into three settings is three chances to have them\ndisagree. A domain that isn't `*.cloudflareaccess.com` is refused rather than accepted,\nsince it would send both token verification and agent sign-in somewhere unintended.\n\n**For agents, turn on [Managed OAuth](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/)\non the Access application.** Access then acts as the OAuth authorization server itself —\nincluding dynamic client registration, which the MCP spec expects and which a plain\nAccess SaaS/OIDC app does not do. Without it, an agent hitting `/mcp` gets Access's HTML\nlogin page, which it cannot do anything with.\n\nWith managed OAuth on, Access answers the 401 at the edge, so **Trove's own challenge is\nnever reached** — that is fine and intended, and it is why Trove doesn't need to be\nconfigured differently for it. What arrives at the origin is the resolved\n`Cf-Access-Jwt-Assertion`, which Trove verifies exactly as it does a browser's.\n\nOne subtlety worth knowing, because it is invisible when it goes wrong: the agent's token\nunder managed OAuth is **opaque**, not a JWT, and it travels in `Authorization: Bearer`.\nThe real signed JWT is the assertion header. Trove reads the assertion header *first* for\nthat reason — and because it is the one the edge vouched for, rather than the one the\ncaller typed.\n\n#### Who your users are, to Trove\n\nAccess mints a token whose `sub` is an **internal user UUID**, not an address. That UUID\nbecomes `principal.id`; the address lands in the `email` claim. So anywhere you write a\nperson down — `TROVE_ADMINS`, a collection's `user` grant — **either works**, and the\nemail is the one to reach for.\n\n```sh\ncurl -s https://drive.example.com/api/me | jq\n# { \"principal\": { \"id\": \"8f2a1c04-…\",     # Access user UUID  (the `sub` claim)\n#                  \"email\": \"you@example.com\",\n#                  \"roles\": [] },\n#   \"authenticated\": true, \"admin\": true }\n```\n\n`roles` comes from a `roles` or `groups` claim. Access does not send one by default —\nadd it to the application's OIDC claims if you want `TROVE_COLLECTION_CREATOR_ROLES` or\nrole-based grants to have anything to match.\n\n## Connecting an AI agent (MCP)\n\nTrove speaks the [Model Context Protocol](https://modelcontextprotocol.io) at `/mcp`.\nPoint an assistant at that URL and it can search the drive, read and write files, and\nfollow the `trove:` links between them. The tools go through the same permission checks\nthe web app does, so an agent sees exactly what the person whose token it holds sees —\nthere is no service account and no MCP-shaped path around the collection ACL.\n\n```sh\n# What to paste into an assistant, and whether it needs a token:\ncurl http://localhost:8787/api/capabilities | jq '.mcp, .auth'\n```\n\nOn an open drive (the zero-config default) an agent just connects. Demanding a bearer\ntoken from an agent for a drive that demands none from a browser would protect nothing.\n\n### Authentication\n\nWhen the drive has a real identity provider, MCP requires the **same JWT** the browser\npresents. An agent that hasn't got one discovers where to get it, per\n[RFC 9728](https://www.rfc-editor.org/rfc/rfc9728.html), which is what the MCP\nauthorization spec builds on:\n\n```\n$ curl -i -X POST http://localhost:8787/mcp -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'\nHTTP/1.1 401 Unauthorized\nWWW-Authenticate: Bearer realm=\"Trove\", error=\"invalid_token\",\n  error_description=\"...\",\n  resource_metadata=\"http://localhost:8787/.well-known/oauth-protected-resource/mcp\"\n\n$ curl http://localhost:8787/.well-known/oauth-protected-resource/mcp\n{\"resource\":\"http://localhost:8787/mcp\",\n \"authorization_servers\":[\"https://auth.example.com\"],\n \"scopes_supported\":[\"trove:read\",\"trove:write\"],\n \"bearer_methods_supported\":[\"header\"]}\n```\n\nThe client reads that, runs the OAuth flow at your authorization server, and comes back\nwith a token. Trove never runs the login itself — it verifies what your IdP issued.\n\n**This is the drive's authorization server, not MCP's.** \"Where do I sign in\" is a\nproperty of the deployment, so a 401 from `/api/items` carries the same challenge, and\n`/.well-known/oauth-protected-resource` describes the drive itself. One setting, two\nsurfaces, no way for them to disagree — see [Where clients sign in](#where-clients-sign-in).\n\n| variable | what it does |\n| --- | --- |\n| `TROVE_MCP` | `off` to disable the endpoint entirely |\n| `TROVE_MCP_RESOURCE` | the canonical public URL, when a proxy rewrites the Host |\n| `TROVE_MCP_PATH` | serve it somewhere other than `/mcp` |\n| `TROVE_MCP_REQUIRE_AUTH` | force auth on or off, rather than following the drive |\n\nNote what is *not* in that table: the authorization server. Those four are all about\n*this endpoint* — where it lives and whether it demands a token. Where the token comes\nfrom belongs to the drive.\n\n### Tools\n\n| tool | |\n| --- | --- |\n| `search_files` | semantic + keyword search, `#tag` filters, across everything readable |\n| `list_files` | page through a collection |\n| `read_file` | text by id, name, or `trove:` URI |\n| `write_file` | create or replace (needs write) |\n| `delete_file` | to the trash, recoverable (needs delete) |\n| `list_collections` | what you can see, and what you may do in each |\n| `get_file_info` | type, size, tags, and backlinks |\n\nFiles are also exposed as MCP **resources** under their `trove:` URIs, for clients that\nattach context rather than calling tools.\n\nThe server tells the model up front that this drive has no folders — otherwise every\nassistant spends its first few turns constructing paths that don't exist.\n\n## Using the core as a library\n\nEvery backend is a provider you inject into the server (or `createVfs`) — pass a\nclass instance, or a `{ driver, ... }` config the server builds for you:\n\n```js\nimport { createServer } from '@3sln/trove';\nimport { S3Storage, SqliteStore, HttpEmbedding, QdrantVectorStore } from '@3sln/trove/core';\n\nconst { handle } = await createServer({\n  storage:     new S3Storage({ bucket, region, accessKeyId, secretAccessKey }),\n  metadata:    new SqliteStore({ path: 'trove.db' }),\n  embeddings:  new HttpEmbedding({ url, apiKey, model, dimensions: 1536 }),\n  vectorStore: new QdrantVectorStore({ url, collection: 'trove', dimensions: 1536 }),\n});\n```\n\n### Writing a custom driver\n\nEvery seam is a small async class. Subclass it, pass the instance in, and the server\nuses it — there is no registration step and no factory to teach about it, because\n`resolve()` takes either an instance or a `{ driver }` config and an instance always\nwins.\n\n| you want to change | implement | the methods that matter |\n| --- | --- | --- |\n| where bytes live | `StorageBackend` | `put` `get` `delete` `list` `head` (+ `presign*`, `usage` if you can) |\n| where records live | `MetadataStore` | `create` `getById` `listItems` `rename` `remove` `findByTags` … |\n| the vector index | `VectorStore` | `add` `query` `remove{,ByNode,ByIndexer,ByNodeIndexer}` |\n| the keyword index | `KeywordStore` | `add` `search` `remove*` `count` |\n| how text becomes vectors | `EmbeddingProvider` | `embed(texts) -> number[][]` |\n| SQL (D1, Turso, Postgres…) | `SqliteProvider` + `SqliteDatabase` | `obtain` / `exec` `run` `get` `all` `batch` |\n| who the caller is | `IdentityProvider` | `authenticate(request) -> Principal \\| null` |\n| what a search query means | `SearchTransformer` | `transform(raw, ctx)` and `describe()` |\n| shared small state | `KeyValueStore` | `get` `set` `delete` `list` |\n\n```js\nimport { createServer } from '@3sln/trove';\nimport { VectorStore } from '@3sln/trove/core';\n\nclass PgVectorStore extends VectorStore {\n  constructor(pool, { dimensions }) { super(); this.pool = pool; this.dimensions = dimensions; }\n  async add(docs) { /* upsert (id, nodeId, indexerId, vector) */ }\n  async query(vector, { limit = 20, collectionIds } = {}) {\n    // return [{ id, nodeId, indexerId, score }] — score higher-is-better\n  }\n  async removeByNode(nodeId) { /* … */ }\n  // removeByIndexer / removeByNodeIndexer / remove likewise\n}\n\nconst { handle } = await createServer({\n  vectorStore: new PgVectorStore(pool, { dimensions: 1536 }),\n});\n```\n\nTwo conventions the interfaces rely on, both of which will bite quietly if ignored:\n\n- **Say what you can't do rather than pretending.** `StorageBackend.capabilities`\n  advertises `presignDownload`, `list`, `usage` and friends, and callers branch on it —\n  an S3 deployment uploads straight to the bucket while a filesystem one proxies,\n  from the same client code. A backend that can't report free space returns `null` from\n  `usage()` and the UI shows no gauge, rather than a meter built from a guess.\n- **`durable` is a claim, not an inference.** A `SqliteProvider` that says `false` gets\n  the in-memory search stores, because an index in an ephemeral database is worse than\n  one in memory: it looks persistent right until the restart that proves it isn't.\n\n`D1SqliteProvider` is worth reading as a worked example — it is the whole\n`SqliteProvider` + `SqliteDatabase` pair against a database with a slightly different\ndialect, in about a hundred lines, and its tests run the real `SqliteStore` and\n`SqliteKV` against a D1-shaped shim.\n\nOr drive the built-in drivers from env:\n\n```sh\nTROVE_VECTOR=qdrant TROVE_QDRANT_URL=http://localhost:6333 \\\nTROVE_QDRANT_COLLECTION=trove node packages/server/src/adapters/node.js\n```\n\n### Where the search index lives\n\n`TROVE_VECTOR` (`sqlite` | `memory` | `qdrant` | `vectorize`) and `TROVE_KEYWORD`\n(`sqlite` | `memory`) pick the stores. You normally set neither: a deployment with\na SQLite database gets the durable SQLite stores, and one with nothing to persist\nto gets the in-memory ones — an index in an ephemeral database is worse than one\nin memory, because it looks persistent until the restart that proves it isn't.\n`GET /api/capabilities` reports which stores are in use and whether they're\n`durable`.\n\nFTS5 (keywords) is compiled into both `bun:sqlite` and `node:sqlite`, so there is\nnothing to install. `sqlite-vec` (vectors) is a prebuilt native artifact and\ntherefore an **optional** dependency: if it can't load on your platform, Trove\nlogs a warning, keeps keyword search durable, and falls back to an in-memory\nvector index rather than refusing to start.\n\nThe lower-level `createVfs` helper does the same wiring for library use:\n\n```js\nimport { createVfs } from '@3sln/trove/core';\nconst vfs = await createVfs({ storage, metadata, embeddings, vectorStore });\nawait vfs.writeFile('root', 'note.txt', 'hello');\nconst hits = await vfs.searchQuery('greeting');\n```\n\n## Writing a plugin\n\nA plugin is a **ZIP** containing a `manifest.json`, an entry script, and any\nassets. The manifest declares the plugin's id, the capabilities it wants, its\ncontributions, and its settings:\n\nCapabilities are declared as an object — each key is a capability, each value is\nthat capability's **options**. A capability that takes no options uses `true` (an\nempty object works too); the `network` capability carries its allowed endpoint\nprefixes:\n\n```json\n{\n  \"id\": \"com.example.hello\",\n  \"name\": \"Hello\",\n  \"version\": \"1.0.0\",\n  \"entry\": \"plugin.js\",\n  \"domain\": \"plugins.example.com\",\n  \"capabilities\": {\n    \"ui\": true,\n    \"commands\": true,\n    \"storage\": { \"plugin\": true, \"domain\": false },\n    \"indexer\": true,\n    \"network\": { \"endpoints\": [\"https://api.example.com/v1/\"] }\n  },\n  \"settings\": [{ \"key\": \"apiKey\", \"type\": \"string\", \"title\": \"API key\", \"secret\": true }]\n}\n```\n\nA package can be a single entry script (`entry: \"plugin.js\"`) or **multiple ES\nmodules**: put your code under `src/` and use ordinary relative imports — no\nbundler required. The host loads every `src/*.js` file as a `blob:` module inside\nthe sandbox and wires them with an import map, so `import './lib/util.js'` and\n`import { activate } from 'trove'` both resolve; everything outside `src/` is an\nopaque asset you read via `ctx.resources`.\n\n```\nmy-plugin.zip\n├─ manifest.json          # \"entry\": \"src/index.js\"\n├─ src/index.js           # imports ./lib/http.js, 'trove'\n├─ src/lib/http.js\n└─ assets/banner.png      # read via ctx.resources, not importable\n```\n\nThe host injects `@3sln/trove/plugin-sdk` into the sandboxed frame; the entry script\ncalls `trove.activate` (or `import { activate } from 'trove'`):\n\n```js\ntrove.activate(async (ctx) => {\n  // Contribute a command, a status item, an opener, or an indexer.\n  ctx.commands.register('hello.world', () => ctx.ui.toast('Hi from a plugin!'));\n\n  // Read a packaged asset via an opaque handle (no URLs leak out of the frame).\n  const banner = await ctx.resources.text('banner.txt');\n\n  // Persist state in the plugin's own SQLite db (declare \"storage\"). Each scope\n  // has a `.server` (online) and `.client` (on-device, offline) handle.\n  const db = ctx.storage.plugin.server;\n  await db.exec('CREATE TABLE IF NOT EXISTS state (k TEXT PRIMARY KEY, v TEXT)');\n  const row = await db.get('SELECT v FROM state WHERE k = ?', 'count');\n  await db.run('INSERT OR REPLACE INTO state VALUES (?, ?)', 'count', String(Number(row?.v || 0) + 1));\n\n  // Read a secret the user entered in settings (never stored in plaintext prefs).\n  const key = await ctx.settings.getSecret('apiKey');\n\n  // Reach the web only through the host, and only to declared endpoints (\"network\").\n  const res = await ctx.net.fetch('https://api.example.com/v1/status', {\n    headers: { Authorization: `Bearer ${key}` },\n  });\n  const data = await res.json();\n\n  // Push search documents under this plugin's namespace (declare \"indexer\").\n  ctx.contributes.indexer({ id: 'labels', title: 'Image labels' });\n  await ctx.files.index('labels', nodeId, [{ text: 'golden retriever, park' }], { tags: [...] });\n});\n```\n\nCapabilities the manifest doesn't request (or the user doesn't grant) are simply\nabsent from `ctx`. To ship a **domain-verified** plugin, sign the package and\npublish the key's fingerprint at `https://<domain>/.well-known/trove-assetlinks.json`.\nSee `packages/web/test/pluginFixture.mjs` for a complete, self-contained example\npackage.\n\nInside the sandbox the host injects the SDK and exposes it as the global `trove`.\nWhen you build or bundle your plugin outside the sandbox, `import { activate } from\n'@3sln/trove/plugin-sdk'` resolves to the **same implementation** — the package entry is\na thin re-export of the injected build, so there's no drift between what you import\nand what actually runs.\n\n## Layout\n\n```\npackages/\n  core/         @3sln/trove/core — Vfs, storage/metadata/search backends, uploads (runtime-agnostic)\n  server/       @3sln/trove/server — Request→Response API + Bun / Node / Worker adapters\n  web/          @3sln/trove/web — the workbench (dodo + ngin)\n  plugin-sdk/   @3sln/trove/plugin-sdk — the iframe-side plugin API + RPC\n  create-trove/ @3sln/create-trove — the scaffolder; the one directory that is\n                its own published package rather than part of @3sln/trove\nplugins/\n  audiobook/    a plugin we ship, built into a signed zip\n```\n\n`plugins/` is a BUILD location rather than a workspace, and the difference is the point: a\nplugin's artifact is a zip the server independently re-parses and installs, never an npm\ndependency and never imported from `packages/`. `bun run build:plugins` produces the same\nbytes a user would drag into the install dialog — there is no privileged path for our own.\nNothing there is preinstalled: a drive with a plugin its owner did not choose is a drive\nwith a capability grant its owner did not make. See `plugins/README.md`.\n\n## Releasing\n\nTwo packages, one version:\n\n```sh\nnpm version patch      # or minor / major — bumps BOTH manifests in one commit\ngit push --follow-tags\n```\n\n`@3sln/trove` and `@3sln/create-trove` are released together and carry the same number,\nwhich is what lets the scaffolder pin the exact drive it shipped alongside by reading its\nown version. npm has no way to share a version between manifests, so the root is the\nsource of truth and the `version` lifecycle script copies it down\n(`scripts/sync-version.mjs`) and stages the result — there is no second edit to remember.\n`npm run sync-version` does it on demand after a hand-edit; a unit test catches drift on\nevery pull request, and `publish.yml` checks again before the registry.\n\nPushing the bump to `main` opens a **draft** release. Publishing that draft creates the\ntag and runs `publish.yml`, which tests, builds the web app, verifies both tarballs\nactually contain what makes them work, and publishes each package — skipping either one\nthat is already on the registry, so a half-failed release can simply be published again.\n\n## Tests\n\n```sh\nbun test                                        # node-level units: core, server, plugin-sdk, mp4, plugin packages/signing\nnpm run test:browser --prefix packages/web      # web units in real Chromium (@web/test-runner): signing, module graph, zip\nnode packages/web/test/e2e.mjs                  # full workbench in headless Chromium\nnode packages/web/test/plugins.e2e.mjs          # sandboxed plugin install, brokered network, offline availability\nnode packages/web/test/offline.e2e.mjs          # service worker, pinning, offline queue + sync\nnode packages/web/test/multiuser.e2e.mjs       # access boundaries across 4 users & 2 collections, API + UI\nnode packages/web/test/probe/run-all.mjs        # error-path probes: broken openers, server faults, retry, uninstall failure, opener choice, activity/issues\nnode packages/web/test/probe/walkthrough.mjs    # full in-browser user journey + screenshots (test/screens/)\n```\n\nThe web unit suites run under **both** `bun test` (fast, via `test/testkit.js`) and\n`@web/test-runner` (a real browser, the platform plugins ship to) — the same files,\nno duplication.\n\n**Probes** (`test/probe/`) cover the error paths a ha","readmeFilename":"README.md"}