{"_id":"@alis-kit/storage-client","_rev":"3-d0452e3b9ae1308064b05083ccb1bd90","name":"@alis-kit/storage-client","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@alis-kit/storage-client","version":"1.0.0","keywords":["storage","file-upload","s2s","service-to-service","local-storage","presigned-url"],"license":"MIT","_id":"@alis-kit/storage-client@1.0.0","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"dist":{"shasum":"af1a8fc942d0333be76a793b234157db5175f79b","tarball":"https://registry.npmjs.org/@alis-kit/storage-client/-/storage-client-1.0.0.tgz","fileCount":26,"integrity":"sha512-v0RP0U6khtOBMOHxZZqKJwx+wVpxn1BDEQw9j39UA3f72tG6gb2Zub2Er/4gXa+xwOKVhzkrK/epUE0vXTxDRw==","signatures":[{"sig":"MEYCIQDrbFw5o2wgmWzuvXgpwtzfZWn4PFPoE4RuNB03CoxE/wIhAOjcOY4r7IdXil3JRB6haXpIjpLAjaOtKc8MD8UQclrB","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37409},"main":"./dist/index.js","type":"module","_from":"file:alis-kit-storage-client-1.0.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20.19.0","typescript":">=5.6"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\a48a11e141a8bd1015dc6c9575945f26\\alis-kit-storage-client-1.0.0.tgz","_integrity":"sha512-v0RP0U6khtOBMOHxZZqKJwx+wVpxn1BDEQw9j39UA3f72tG6gb2Zub2Er/4gXa+xwOKVhzkrK/epUE0vXTxDRw==","_npmVersion":"11.6.2","description":"Pluggable file storage client — local disk or S2S relay to a remote storage service, behind one interface","directories":{},"_nodeVersion":"24.12.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.10","fastify":"^5.10.0","typescript":"^7.0.2","@types/node":"^26.1.1"},"optionalDependencies":{"fastify":"^5.10.0"},"_npmOperationalInternal":{"tmp":"tmp/storage-client_1.0.0_1787562289172_0.13537564216907105","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@alis-kit/storage-client","version":"1.1.0","keywords":["storage","file-upload","s2s","service-to-service","local-storage","presigned-url"],"license":"MIT","_id":"@alis-kit/storage-client@1.1.0","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"dist":{"shasum":"87ed4ba1be97688ef6c19e29a82520f35bb5c98f","tarball":"https://registry.npmjs.org/@alis-kit/storage-client/-/storage-client-1.1.0.tgz","fileCount":26,"integrity":"sha512-fVfcDHU6MQ5N598sMYV0xrodhqnD5BcCcnfGdFOB3UtJa6hQv+CgdrQqHeWcW5YMP2XxTHI2nGWa1mo46ZsbJg==","signatures":[{"sig":"MEUCIQDRF/niBC2rfAfLRVyx+85/KUYIH/bQ67sgTt/TId0yogIgCkIcNj9TB8xfpPfi5V3yAfBmZ032Bkb8mDAJ2m4UEkQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":40075},"main":"./dist/index.js","type":"module","_from":"file:alis-kit-storage-client-1.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20.19.0","typescript":">=5.6"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\7f06d2faf07c447e62e68cdceeb07420\\alis-kit-storage-client-1.1.0.tgz","_integrity":"sha512-fVfcDHU6MQ5N598sMYV0xrodhqnD5BcCcnfGdFOB3UtJa6hQv+CgdrQqHeWcW5YMP2XxTHI2nGWa1mo46ZsbJg==","_npmVersion":"11.6.2","description":"Pluggable file storage client — local disk or S2S relay to a remote storage service, behind one interface","directories":{},"_nodeVersion":"24.12.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.10","fastify":"^5.10.0","typescript":"^7.0.2","@types/node":"^26.1.1"},"optionalDependencies":{"fastify":"^5.10.0"},"_npmOperationalInternal":{"tmp":"tmp/storage-client_1.1.0_1787758356731_0.33946491296620174","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@alis-kit/storage-client","version":"2.0.0","description":"Pluggable file storage client — local disk or S2S relay to a remote storage service, behind one interface","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"engines":{"node":">=20.19.0","typescript":">=5.6"},"keywords":["storage","file-upload","s2s","service-to-service","local-storage","presigned-url"],"license":"MIT","dependencies":{},"devDependencies":{"@types/node":"^26.1.1","fastify":"^5.10.0","typescript":"^7.0.2","vitest":"^4.1.10"},"optionalDependencies":{"fastify":"^5.10.0"},"scripts":{"build":"tsc","dev":"tsc --watch","test":"vitest run","test:watch":"vitest","lint":"tsc --noEmit"},"_id":"@alis-kit/storage-client@2.0.0","_integrity":"sha512-lpRV/5klyxShGpnoQD8Vxr8b5QdoQoqKitqOHpoMsw7niQFhGp8R4fnOr1Cdl0hw1S6R5YupKc+AoKbQhXBPyA==","_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\506b21abb02f3ce0514438f0ca21e2d8\\alis-kit-storage-client-2.0.0.tgz","_from":"file:alis-kit-storage-client-2.0.0.tgz","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-lpRV/5klyxShGpnoQD8Vxr8b5QdoQoqKitqOHpoMsw7niQFhGp8R4fnOr1Cdl0hw1S6R5YupKc+AoKbQhXBPyA==","shasum":"e2cfbeacfddf2d2976c18695e89c6537f0e08128","tarball":"https://registry.npmjs.org/@alis-kit/storage-client/-/storage-client-2.0.0.tgz","fileCount":26,"unpackedSize":46616,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDcTq0hL5ZEhhxUKhZejGG+N5kuAuQs9RC8kyeFlkPxRgIhANozN4Lz0qh31owFHapYIlVthpjXjYFIiJee7u9sPacm"}]},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"directories":{},"maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/storage-client_2.0.0_1788161503104_0.03865003899310482"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-24T09:04:47.937Z","modified":"2026-08-31T07:31:43.527Z","1.0.0":"2026-08-24T09:04:49.320Z","1.1.0":"2026-08-26T15:32:36.843Z","2.0.0":"2026-08-31T07:31:43.244Z"},"license":"MIT","keywords":["storage","file-upload","s2s","service-to-service","local-storage","presigned-url"],"description":"Pluggable file storage client — local disk or S2S relay to a remote storage service, behind one interface","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"readme":"# @alis-kit/storage-client\n\nPluggable file storage client for Node.js — one interface, two interchangeable\ndrivers: **`local`** (disk on the same server, no external dependency) and\n**`s2s`** (relay to a remote storage service over HTTP, service-to-service).\nSwitch drivers via configuration; calling code never branches on which one is\nactive.\n\n[![npm version](https://img.shields.io/npm/v/@alis-kit/storage-client.svg)](https://www.npmjs.com/package/@alis-kit/storage-client)\n[![license](https://img.shields.io/npm/l/@alis-kit/storage-client.svg)](#license)\n\n---\n\n## Table of Contents\n\n- [Why](#why)\n- [Install](#install)\n- [Quick Start](#quick-start)\n- [Driver: `local`](#driver-local)\n- [Driver: `s2s`](#driver-s2s)\n- [Separating files by folder (`folderId`)](#separating-files-by-folder-folderid)\n- [Serving local files over Fastify](#serving-local-files-over-fastify)\n- [Error Handling](#error-handling)\n- [Design Notes](#design-notes)\n- [License](#license)\n\n---\n\n## Why\n\nAn app that stores files (photos, exports, attachments) shouldn't have to\nchoose its storage backend at every call site. This package gives you one\n`StorageDriver` interface —\n\n```ts\ninterface StorageDriver {\n  uploadFile(stream: Readable, meta: UploadFileMeta): Promise<UploadedStorageFile>;\n  getDownloadLink(fileId: string): Promise<StorageDownloadLink>;\n  deleteFile(fileId: string): Promise<void>;\n}\n```\n\n— and two implementations of it. Start with `local` (zero external services),\nmove to `s2s` (a shared storage service, e.g. quota/versioning/thumbnails\nhandled centrally) later, without touching the code that calls\n`uploadFileToStorage`/`getFileDownloadLink`.\n\n## Install\n\n```bash\nnpm install @alis-kit/storage-client\n```\n\nNo hard dependency on a web framework or a database. `fastify` is an\n**optional** dependency, needed only if you use the bundled\n[Fastify download handler](#serving-local-files-over-fastify).\n\n## Quick Start\n\n```ts\nimport { createStorageClient } from '@alis-kit/storage-client';\n\n// Pick the driver from your own env/config — this is the ONLY place that\n// needs to know which one is active.\nconst storage = createStorageClient(\n  process.env.STORAGE_DRIVER === 's2s'\n    ? {\n        driver: 's2s',\n        s2s: {\n          baseUrl: process.env.FILE_STORAGE_API_BASE_URL!,\n          apiKey: process.env.FILE_STORAGE_API_KEY!,\n        },\n      }\n    : {\n        driver: 'local',\n        local: {\n          storageDir: process.env.LOCAL_STORAGE_DIR ?? './storage-data',\n          buildDownloadUrl: (fileId) => `${process.env.PUBLIC_BASE_URL}/storage/local/${fileId}`,\n        },\n      },\n);\n\n// Everywhere else in your app:\nconst uploaded = await storage.uploadFile(fileStream, {\n  fileName: 'laporan.pdf',\n  sizeBytes: 204_800,\n  mimeType: 'application/pdf',\n});\n\nconst link = await storage.getDownloadLink(uploaded.fileId);\n// link.url, link.previewUrl, link.expiresAt, link.fileName, link.mimeType, link.sizeBytes\n\n// Replacing a file (e.g. new avatar)? Delete the old one — idempotent, so a\n// fileId that's already gone doesn't need its own try/catch:\nawait storage.deleteFile(previousFileId);\n```\n\n## Driver: `local`\n\nStores the file on disk under `storageDir`; metadata (original name, MIME\ntype, size) is kept as a JSON sidecar next to the blob — **no database\nrequired**.\n\n```ts\nimport { createLocalStorageDriver } from '@alis-kit/storage-client';\n\nconst driver = createLocalStorageDriver({\n  storageDir: './storage-data',\n  // Optional — omit if you'll serve files yourself via `link.filePath`.\n  buildDownloadUrl: (fileId) => `https://api.example.com/storage/local/${fileId}`,\n});\n```\n\n- Upload is streamed to disk (`fs.createWriteStream` + `pipeline`), never\n  buffered fully in memory.\n- The bytes actually written are verified against the declared `sizeBytes`;\n  a mismatch throws `StorageClientError` and removes the partial file.\n- `getDownloadLink(fileId).filePath` is the absolute path on disk — read it\n  directly if you're serving the file from the same process (see the\n  [Fastify handler](#serving-local-files-over-fastify) below), or use it with\n  any framework's own static/stream response.\n- `expiresAt` is always `null` — this driver never signs URLs. Authorization\n  for `GET /storage/local/:fileId` is your app's responsibility (e.g. gate it\n  behind your existing session auth).\n- `getDownloadLink(fileId)` rejects any `fileId` that resolves outside\n  `storageDir` (e.g. `../../.env`) with `StorageClientError` 404\n  (`FILE_NOT_FOUND`) instead of reading it — relevant because `fileId` often\n  comes straight from a public URL param (see\n  [Serving local files over Fastify](#serving-local-files-over-fastify)).\n- `deleteFile(fileId)` removes the blob and its `.meta.json` sidecar.\n  Idempotent — a `fileId` that doesn't exist (or resolves outside\n  `storageDir`) resolves without error rather than throwing.\n\n## Driver: `s2s`\n\nRelays uploads/downloads to a remote storage service over HTTP,\nservice-to-service — your app never touches the file bytes directly for\nmetadata, and the API key never reaches end users.\n\n```ts\nimport { createS2sStorageDriver } from '@alis-kit/storage-client';\n\nconst driver = createS2sStorageDriver({\n  baseUrl: 'https://storage.example.com/api',\n  apiKey: process.env.FILE_STORAGE_API_KEY!,\n  onError: (event) => logger.error(event, 'storage client error'), // optional\n});\n```\n\nExpects the remote service to implement this contract (matches the `s2s`\nmodule of a Fastify + `@alis-kit/routers` storage service, but any backend\nfollowing the same shape works):\n\n| Method | Path | Auth | Purpose |\n|---|---|---|---|\n| `POST` | `/s2s/upload-links` | `X-Api-Key` | Request a short-lived presigned upload URL |\n| `PUT` | `<uploadUrl>` | signature in query | Upload the raw bytes |\n| `GET` | `/s2s/files/:fileId/download-link` | `X-Api-Key` | Request a presigned download URL |\n| `DELETE` | `/s2s/files/:fileId` | `X-Api-Key` | Delete a file |\n\nResponses are expected as `{ message: string, data: T | null }` — `data` is\n`null` on error, and `message` is used as the error text. `DELETE` may also\nreply `204 No Content` with no body.\n\n- Upload is streamed via `Readable.toWeb()` + `fetch({ duplex: \"half\" })` —\n  never buffered fully in memory, safe for large files.\n- Both request failures (network unreachable) and non-2xx responses throw\n  `StorageClientError`; pass `onError` to hook in your own logger.\n- `deleteFile(fileId)` is idempotent — a `404` response from the remote\n  service is treated as success, matching the `local` driver's behavior.\n\n## Separating files by folder (`folderId`)\n\nPass `folderId` in `UploadFileMeta` to keep uploads from different features,\ntenants, or owners from landing in one flat pile — there's no fixed\nconvention for the value, any consuming app/call site picks its own string:\n\n```ts\nconst uploaded = await storage.uploadFile(fileStream, {\n  fileName: 'foto-profil.jpg',\n  sizeBytes: 51_200,\n  mimeType: 'image/jpeg',\n  folderId: `profile-photo/${userId}`, // any string you like — not a fixed schema\n});\n```\n\nBehavior differs by driver, but calling code never needs to know which one is\nactive:\n\n- **`local`** — `folderId` becomes a real subdirectory under `storageDir`\n  (created automatically, `mkdir` recursive, before the file is written). It's\n  also encoded into the returned `fileId` (`<folderId>/<uuid>`), so\n  `getDownloadLink(fileId)` finds the file again without any extra state on\n  your side — just persist `fileId` as usual (e.g. `photoFileId` on a user\n  record), nothing else changes in how you call the API.\n  `folderId` is sanitized before touching the filesystem (`..` segments and\n  absolute paths are rejected with `StorageClientError` `INVALID_FOLDER_ID`),\n  so don't build it from unsanitized user input beyond an id/slug you control.\n- **`s2s`** — `folderId` is forwarded as-is to the remote storage service's\n  `POST /s2s/upload-links` call; the separation happens on that service's\n  side (e.g. it may group files under it, or use it purely for reporting).\n\nOmitting `folderId` (or passing `null`) keeps the previous flat behavior —\nthis is fully backward compatible, existing callers don't need to change.\n\n## Serving local files over Fastify\n\n```ts\nimport { createLocalDownloadHandler } from '@alis-kit/storage-client';\n\napp.get('/storage/local/:fileId', { preHandler: yourAuthGuard }, createLocalDownloadHandler(driver));\n```\n\n`fastify` is only imported as a **type** here — calling this function does\nnot require `fastify` to be installed unless you actually use it as your web\nframework (it's an `optionalDependency` of this package for that reason).\nUsing it against an `s2s` driver always 404s (`filePath` is only ever set by\nthe `local` driver — the file genuinely isn't on this machine).\n\n## Error Handling\n\nEvery thrown error is a `StorageClientError`:\n\n```ts\nexport class StorageClientError extends Error {\n  readonly statusCode: number; // suggested HTTP status\n  readonly code: string;       // e.g. \"FILE_NOT_FOUND\", \"STORAGE_UNREACHABLE\"\n}\n```\n\nMap it to your own error class at the boundary if you want a single error\nhierarchy across your app:\n\n```ts\ntry {\n  await storage.uploadFile(stream, meta);\n} catch (error) {\n  if (error instanceof StorageClientError) {\n    throw new AppError(error.statusCode, error.code, error.message);\n  }\n  throw error;\n}\n```\n\n## Design Notes\n\n- **Both drivers implement the exact same `StorageDriver` interface** — this\n  is the whole point. A third driver (S3, GCS, …) is just another\n  implementation of that interface plus a branch in `createStorageClient`;\n  nothing else in a consuming app changes.\n- **No framework/database lock-in.** The core (`types.ts`, both drivers,\n  `storage-client.ts`) has zero runtime dependencies. `fastify` is optional\n  and type-only unless you call `createLocalDownloadHandler`.\n- **`local` driver has no database dependency on purpose** — the JSON\n  sidecar keeps this package usable in any app regardless of what ORM/DB they\n  use. If you need richer local metadata (tags, search, per-file ownership\n  records), wrap this driver in your own layer rather than extending it here\n  — folder-level separation (`folderId`) is covered natively (see above), but\n  arbitrary querying/indexing is not this package's job.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}