[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-84012":3},{"id":4,"name":5,"fullName":6,"owner":5,"repo":5,"description":7,"homepage":8,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":12,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":12,"stars7d":15,"stars30d":15,"stars90d":14,"forks30d":14,"starsTrendScore":16,"compositeScore":17,"rankGlobal":9,"rankLanguage":9,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":9,"pushedAt":9,"updatedAt":34,"readmeContent":35,"aiSummary":9,"trendingCount":14,"starSnapshotCount":14,"syncStatus":12,"lastSyncTime":36,"discoverSource":37},84012,"storagesdk","storagesdk\u002Fstoragesdk","A unified TypeScript SDK for storage with first-class support for snapshotting, forking across Tigris, Amazon S3, Cloudflare R2, GCS, Azure Blob, Vercel Blob and many more.","https:\u002F\u002Fstoragesdk.dev\u002F",null,"TypeScript",64,2,1,0,9,13,1.43,"Apache License 2.0",false,"main",true,[23,24,25,26,27,28,29,30,31,32,33],"esm","minio","nodejs","object-storage","r2","s3","sdk","snapshots","storage","tigris","typescript","2026-06-12 02:04:37","# storagesdk\n\n[![npm version](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002F@storagesdk\u002Fcore?label=%40storagesdk%2Fcore)](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@storagesdk\u002Fcore)\n[![CI](https:\u002F\u002Fgithub.com\u002Fstoragesdk\u002Fstoragesdk\u002Factions\u002Fworkflows\u002Fci.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fstoragesdk\u002Fstoragesdk\u002Factions\u002Fworkflows\u002Fci.yml)\n[![license](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fl\u002F@storagesdk\u002Fcore)](.\u002FLICENSE)\n\nA unified TypeScript SDK for storage with first-class support for snapshotting, forking across Tigris, Amazon S3, Cloudflare R2, GCS, Azure Blob, Vercel Blob and many more.\n\n```sh\nnpm install @storagesdk\u002Fcore @storagesdk\u002Fadapters\n```\n\n```ts\nimport { Storage } from '@storagesdk\u002Fcore';\nimport { tigris } from '@storagesdk\u002Fadapters\u002Ftigris';\n\nconst storage = new Storage({\n  adapter: tigris({\n    bucket: 'agent-runs',\n    accessKeyId: process.env.TIGRIS_ACCESS_KEY_ID,\n    secretAccessKey: process.env.TIGRIS_SECRET_ACCESS_KEY,\n  }),\n});\n\nawait storage.upload('hello.txt', 'Hello, storage SDK!', {\n  contentType: 'text\u002Fplain',\n});\n\nconst text = await storage.download('hello.txt', { as: 'text' });\nconst url = await storage.url('hello.txt', { expiresIn: 300 });\n\nconst snap = await storage.snapshots.create({ name: 'pre-migration' });\nawait storage.forks.create({ name: 'agent-runs-exp', fromSnapshot: snap.id });\nconst fork = storage.forks.get('agent-runs-exp');\nawait fork.upload('hello.txt', 'mutated in fork only');\n```\n\n## What you get\n\n- **Snapshots and forks as primitives.** Take a snapshot of a bucket, get a read-only handle, fork from it as a writable branch. Native APIs where available (Tigris); sibling buckets\u002Ffolders otherwise.\n- **Typed escape hatch.** `storage.raw` is typed to the underlying SDK (e.g. `S3Client` on the S3 adapter) for provider-specific operations storagesdk doesn't surface.\n- **Agent-ready.** [`@storagesdk\u002Fai`](.\u002Fpackages\u002Fai\u002FREADME.md) wraps every verb (plus the full snapshot and fork roster) as AI tool definitions for the Vercel AI SDK — hand a `Storage` to your agent runtime and get a ready-to-register tool set back.\n- **Runtime adapter selection.** [`@storagesdk\u002Fadapters`](.\u002Fpackages\u002Fadapters\u002FREADME.md#runtime-adapter-selection)'s root export ships `ADAPTERS`, `buildAdapter(name)`, and `getAdapterEnvVars(name)` — CLIs and scripts pick any adapter by name, reading config from adapter-native env vars (`TIGRIS_*`, `S3_*`, etc.) with backend-native fallbacks (`AWS_*`, `BLOB_READ_WRITE_TOKEN`, `GOOGLE_CLOUD_PROJECT`).\n- **ESM-only, Node 20+.** Plain `tsc` build, no bundler.\n\n## Adapters\n\n| Adapter | Subpath | Backend |\n| --- | --- | --- |\n| Tigris | [`@storagesdk\u002Fadapters\u002Ftigris`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Ftigris\u002FREADME.md) | [Tigris](https:\u002F\u002Fwww.tigrisdata.com\u002F) — snapshots and forks are first-class via Tigris's native APIs. |\n| S3 | [`@storagesdk\u002Fadapters\u002Fs3`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Fs3\u002FREADME.md) | Amazon S3 and any S3-compatible provider. |\n| R2 | [`@storagesdk\u002Fadapters\u002Fr2`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Fr2\u002FREADME.md) | [Cloudflare R2](https:\u002F\u002Fwww.cloudflare.com\u002Fdeveloper-platform\u002Fproducts\u002Fr2\u002F). |\n| GCS | [`@storagesdk\u002Fadapters\u002Fgcs`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Fgcs\u002FREADME.md) | [Google Cloud Storage](https:\u002F\u002Fcloud.google.com\u002Fstorage). |\n| Azure Blob | [`@storagesdk\u002Fadapters\u002Fazure`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Fazure\u002FREADME.md) | [Azure Blob Storage](https:\u002F\u002Fazure.microsoft.com\u002Fproducts\u002Fstorage\u002Fblobs). |\n| Vercel Blob | [`@storagesdk\u002Fadapters\u002Fvercel`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Fvercel\u002FREADME.md) | [Vercel Blob](https:\u002F\u002Fvercel.com\u002Fdocs\u002Fvercel-blob). |\n| MinIO | [`@storagesdk\u002Fadapters\u002Fminio`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Fminio\u002FREADME.md) | [MinIO](https:\u002F\u002Fmin.io\u002F). |\n| GitHub | [`@storagesdk\u002Fadapters\u002Fgithub`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Fgithub\u002FREADME.md) | [GitHub](https:\u002F\u002Fgithub.com) repository — snapshots are tags, forks are branches, native git refs all the way down. |\n| WebDAV | [`@storagesdk\u002Fadapters\u002Fwebdav`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Fwebdav\u002FREADME.md) | Any WebDAV server — Nextcloud, ownCloud, Apache mod_dav, nginx-dav, NAS, pCloud, mailbox.org, kDrive. Snapshots\u002Fforks via native server-side `COPY`. |\n| Fly.io | [`@storagesdk\u002Fadapters\u002Ffly`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Ffly\u002FREADME.md) | Fly-managed Tigris buckets — branded alias of the Tigris adapter. |\n| Railway | [`@storagesdk\u002Fadapters\u002Frailway`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Frailway\u002FREADME.md) | [Railway Buckets](https:\u002F\u002Fdocs.railway.com\u002Fstorage-buckets) — branded alias of the Tigris adapter. |\n| Filesystem | [`@storagesdk\u002Fadapters\u002Ffs`](.\u002Fpackages\u002Fadapters\u002Fsrc\u002Ffs\u002FREADME.md) | Local `node:fs\u002Fpromises`. For development and tests. |\n\nFor the full, up-to-date list see **[storagesdk.dev\u002Fadapters](https:\u002F\u002Fstoragesdk.dev\u002Fadapters)**.\n\n## API\n\n```ts\nclass Storage\u003CRaw = unknown> {\n  constructor(opts: { adapter: Adapter\u003CRaw> });\n\n  readonly raw: Raw;\n  readonly snapshots: { create, list, head, delete, get };\n  readonly forks:     { create, list, head, delete, get };\n\n  upload(path: string, body: BodyInput, opts?: UploadOptions): Promise\u003CStorageItemMeta>;\n\n  \u002F\u002F download — single signature returns full StorageItem; overloads return typed bodies\n  download(path: string, opts?: { signal? }):                            Promise\u003CStorageItem>;\n  download(path: string, opts: { as: 'stream', signal? }):               Promise\u003CReadableStream\u003CUint8Array>>;\n  download(path: string, opts: { as: 'text',   signal? }):               Promise\u003Cstring>;\n  download(path: string, opts: { as: 'bytes',  signal? }):               Promise\u003CUint8Array>;\n  download(path: string, opts: { as: 'blob',   signal? }):               Promise\u003CBlob>;\n  download(path: string, opts: { as: 'json',   signal? }):               Promise\u003Cunknown>;\n\n  head(path: string, opts?: { signal? }):                                Promise\u003CStorageItemMeta>;\n  list(opts?: ListOptions):                                              Promise\u003CListResult>;\n  delete(path: string, opts?: { signal? }):                              Promise\u003Cvoid>;\n  copy(from: string, to: string, opts?: { signal? }):                    Promise\u003Cvoid>;\n  move(from: string, to: string, opts?: { signal? }):                    Promise\u003Cvoid>;\n  url(path: string, opts?: UrlOptions):                                  Promise\u003Cstring>;\n  uploadUrl(path: string, opts?: UploadUrlOptions):                      Promise\u003CUploadUrlResult>;\n}\n```\n\n### `snapshots` and `forks`\n\n```ts\nstorage.snapshots.create(opts?: { name?, signal? }):         Promise\u003CSnapshotInfo>;\nstorage.snapshots.list():                                    Promise\u003CSnapshotInfo[]>;\nstorage.snapshots.head(id: string, opts?: { signal? }):      Promise\u003CSnapshotInfo>;\nstorage.snapshots.delete(id: string, opts?: { signal? }):    Promise\u003Cvoid>;\nstorage.snapshots.get(id: string):                           ReadOnlyStorage; \u002F\u002F .download, .head, .list, .url\n\nstorage.forks.create(opts: { name, fromSnapshot?, signal? }): Promise\u003CForkInfo>;\nstorage.forks.list():                                         Promise\u003CForkInfo[]>;\nstorage.forks.head(name: string, opts?: { signal? }):         Promise\u003CForkInfo>;\nstorage.forks.delete(name: string, opts?: { signal? }):       Promise\u003Cvoid>;\nstorage.forks.get(name: string):                              Storage\u003CRaw>;    \u002F\u002F full read\u002Fwrite\n```\n\n### `uploadUrl` — PUT vs POST\n\n```ts\n\u002F\u002F PUT: default. Returns a signed URL the client uploads to with PUT.\nstorage.uploadUrl('photo.jpg', { expiresIn: 300, contentType: 'image\u002Fjpeg' });\n\u002F\u002F → { method: 'PUT', url, headers? }\n\n\u002F\u002F POST: triggered by `maxSize` or `minSize`. Returns a presigned POST URL +\n\u002F\u002F form fields the browser submits as multipart\u002Fform-data. Enforces size and\n\u002F\u002F content-type bounds server-side.\nstorage.uploadUrl('photo.jpg', { expiresIn: 300, maxSize: 5_000_000, contentType: 'image\u002Fjpeg' });\n\u002F\u002F → { method: 'POST', url, fields }\n```\n\n### Errors\n\nEvery operation throws `StorageError`. The `code` is a typed union:\n\n```ts\ntype StorageErrorCode =\n  | 'NotFound'         \u002F\u002F missing key, missing snapshot\u002Ffork\n  | 'NotSupported'     \u002F\u002F adapter doesn't implement this op\n  | 'Conflict'         \u002F\u002F duplicate fork name, etc.\n  | 'Unauthorized'     \u002F\u002F 401\u002F403 from the backend\n  | 'InvalidArgument'  \u002F\u002F bad path, sidecar-suffix collision, etc.\n  | 'Aborted'          \u002F\u002F caller's AbortSignal fired\n  | 'Provider';        \u002F\u002F unmapped backend error (cause attached)\n```\n\n## Common patterns\n\n### Snapshots — read frozen state after live writes\n\n```ts\nawait storage.upload('photo.jpg', 'before');\nconst snap = await storage.snapshots.create({ name: 'baseline' });\nawait storage.upload('photo.jpg', 'after');\n\nconst reader = storage.snapshots.get(snap.id);\nawait reader.download('photo.jpg', { as: 'text' });   \u002F\u002F 'before'\nawait storage.download('photo.jpg', { as: 'text' });  \u002F\u002F 'after'\n```\n\n### Forks — branch and mutate\n\n```ts\nconst snap = await storage.snapshots.create();\nawait storage.forks.create({ name: 'experiment', fromSnapshot: snap.id });\n\nconst fork = storage.forks.get('experiment');\nawait fork.upload('config.json', JSON.stringify({ flag: true }));\n\u002F\u002F parent unchanged; fork has its own writable view\n```\n\n`forks.create` also accepts no `fromSnapshot` — the fork starts at the parent's live state at creation time.\n\n### Signed URLs\n\n```ts\nawait storage.url('photo.jpg', { expiresIn: 300 });          \u002F\u002F 5-min GET URL\nawait storage.uploadUrl('new.jpg', { expiresIn: 300 });      \u002F\u002F PUT URL + method\n```\n\n### Streaming download\n\n```ts\nconst stream = await storage.download('large.mp4', { as: 'stream' });\n\u002F\u002F Web ReadableStream\u003CUint8Array>\n```\n\n### Byte-range reads\n\n```ts\n\u002F\u002F Fetch a slice instead of the full object.\nconst item = await storage.download('video.mp4', {\n  range: { offset: 0, length: 65_536 },\n});\nitem.size; \u002F\u002F 65536 — the slice length, not the full-object size\n\n\u002F\u002F Combines with the `as` overloads.\nconst bytes = await storage.download('big.bin', {\n  as: 'bytes',\n  range: { offset: 4096, length: 1024 },\n});\n```\n\nMaps to each provider's native range API (`Range: bytes=N-M` for S3-family, `download(offset, count)` for Azure, `createReadStream({ start, end })` for GCS, the `Range` header on Vercel). `range` past EOF returns the bytes that exist — matches HTTP `Range` semantics.\n\n### AbortSignal\n\n```ts\nconst ctrl = new AbortController();\nsetTimeout(() => ctrl.abort(), 5000);\n\nawait storage.upload('big.bin', body, { signal: ctrl.signal });\n\u002F\u002F throws StorageError({ code: 'Aborted' }) if signal fires\n```\n\n### Escape hatch\n\n```ts\nconst storage = new Storage({ adapter: tigris({ bucket: 'agent-runs' }) });\n\u002F\u002F    ↑ Storage typed with the underlying client end-to-end, no cast needed\n\nawait storage.raw.someBackendOp({ \u002F* ... *\u002F });\n```\n\n## Examples\n\nRunnable examples live under [`examples\u002F`](.\u002Fexamples). Each picks the adapter at runtime via `EXAMPLE_ADAPTER`; out of the box they run against a local filesystem so you can try them without any setup:\n\n```sh\npnpm install\npnpm --filter @storagesdk\u002Fexamples quickstart\npnpm --filter @storagesdk\u002Fexamples snapshots\npnpm --filter @storagesdk\u002Fexamples forks\npnpm --filter @storagesdk\u002Fexamples agent-with-snapshots  # needs ANTHROPIC_API_KEY for the live agent run\n```\n\n## Authoring adapters\n\n`@storagesdk\u002Fadapters` is *one* set of providers; the SDK is designed for third-party adapters too.\n\n```sh\nnpm install @storagesdk\u002Fcore\n```\n\n```ts\nimport {\n  defineAdapter,\n  type Adapter,\n  StorageError,\n} from '@storagesdk\u002Fcore\u002Fadapter';\n\nexport function myAdapter(config: MyConfig): Adapter {\n  return defineAdapter({\n    name: 'my-backend',\n    raw: \u002F* your client *\u002F,\n    async upload(path, body, opts) { \u002F* ... *\u002F },\n    async download(path, opts) { \u002F* ... *\u002F },\n    async head(path, opts) { \u002F* ... *\u002F },\n    async list(opts) { \u002F* ... *\u002F },\n    async delete(path, opts) { \u002F* ... *\u002F },\n    async copy(from, to, opts) { \u002F* ... *\u002F },\n    async move(from, to, opts) { \u002F* ... *\u002F },\n    async url(path, opts) { \u002F* ... *\u002F },\n    async uploadUrl(path, opts) { \u002F* ... *\u002F },\n    snapshots: { \u002F* create, list, head, delete, get *\u002F },\n    forks:     { \u002F* create, list, head, delete, get *\u002F },\n  });\n}\n```\n\n`@storagesdk\u002Fcore\u002Fadapter` is the adapter-authoring entry. It exposes:\n\n- `defineAdapter` — wraps your implementation with path normalization (leading slashes stripped, empty paths throw) and recursive wrapping for `snapshots.get` \u002F `forks.get` returns.\n- `Adapter`, `ReadOnlyAdapter`, `AdapterSnapshots`, `AdapterForks` — the contract types.\n- `Manifest` helpers (`emptyManifest`, `readManifest`, `writeManifest`, `nextSnapshotId`, `isInternalKey`, `MANIFEST_PATH`) for copy-based adapters that store snapshot\u002Ffork lineage as a sibling location.\n- `checkSignal`, `isAbortError`, `bridgeSignalToController` — abort-handling helpers (Web `AbortSignal` → SDK `AbortController` bridge with listener cleanup).\n- `toWebStream`, `readStreamToBytes` — stream utilities.\n\n### Verifying your adapter\n\nDrop in the conformance test suite:\n\n```sh\nnpm install --save-dev vitest @storagesdk\u002Fadapters\n```\n\n```ts\n\u002F\u002F my-adapter.test.ts\nimport { storageAdapterTestSuite } from '@storagesdk\u002Fadapters\u002Ftest-suite';\nimport { myAdapter } from '.\u002Fmy-adapter.js';\n\nstorageAdapterTestSuite({\n  name: 'my-adapter',\n  adapter: () => myAdapter({ \u002F* config *\u002F }),\n});\n```\n\nThe suite runs the cross-adapter behavioral tests (upload round-trip, NotFound on missing keys, snapshots\u002Fforks contract, AbortSignal short-circuit, etc.) against your adapter. Tests it fails are gaps you need to close.\n\n## Contributing\n\nSee [`AGENTS.md`](.\u002FAGENTS.md) for development setup, gates (lint \u002F typecheck \u002F build \u002F test), and the design decisions that aren't up for re-litigation.\n\n## License\n\n[Apache 2.0](.\u002FLICENSE).\n","2026-06-11 04:12:04","CREATED_QUERY"]