{"_id":"4brains-storage","name":"4brains-storage","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"4brains-storage","version":"1.0.0","description":"Zero-dependency S3-compatible storage client for Garage. No aws-sdk — SigV4 signing and HTTP with Node built-ins only.","main":"index.js","types":"index.d.ts","exports":{".":{"types":"./index.d.ts","import":"./index.mjs","require":"./index.js"}},"engines":{"node":">=18"},"keywords":["garage","s3","object-storage","storage","zero-dependency","sigv4","self-hosted","nas","esm"],"author":{"name":"4Brains"},"license":"MIT","dependencies":{},"_id":"4brains-storage@1.0.0","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-2CJMpyd2fwdePsAxcLcbK4dHHPPOdOauiUFaDCRh3Hs6wTrip7hAMhLIpO8sNeJggJQFE2f3neJFrIqeRt88KQ==","shasum":"316e14e3c99f2b4a112b1d4393becbe374699704","tarball":"https://registry.npmjs.org/4brains-storage/-/4brains-storage-1.0.0.tgz","fileCount":8,"unpackedSize":32724,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCjQ5kTwxbDGeUCcKi7kpWeoJXkKsK4m9BP7VGJuFsduAIgA5/+J0dLIsEOb523RbHFV/CvomlCFvK6hxYJbRn08Qc="}]},"_npmUser":{"name":"4brains-tech","email":"4brains.dev@gmail.com"},"directories":{},"maintainers":[{"name":"4brains-tech","email":"4brains.dev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/4brains-storage_1.0.0_1786102843427_0.9825130886457347"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-07T11:40:43.368Z","1.0.0":"2026-08-07T11:40:43.574Z","modified":"2026-08-07T11:40:43.753Z"},"maintainers":[{"name":"4brains-tech","email":"4brains.dev@gmail.com"}],"description":"Zero-dependency S3-compatible storage client for Garage. No aws-sdk — SigV4 signing and HTTP with Node built-ins only.","keywords":["garage","s3","object-storage","storage","zero-dependency","sigv4","self-hosted","nas","esm"],"author":{"name":"4Brains"},"license":"MIT","readme":"# 4brains-storage\n\nZero-dependency S3-compatible object storage client for Node.js.\n\n**No `aws-sdk`, no dependencies at all** — AWS Signature V4 signing and HTTP are\nimplemented with Node built-ins (`crypto`, `http`/`https`, `stream`).\n\n- Buckets — create, delete, list, check existence\n- Files — upload, download, update, copy, rename, delete, batch delete, list\n- Streaming uploads and downloads (large files never buffer in memory)\n- Multipart uploads\n- Presigned URLs (time-limited links that need no credentials)\n- Works with `import` and `require`; TypeScript definitions included\n- Node.js **18+**\n\n---\n\n## Install\n\n```sh\nnpm i 4brains-storage\n```\n\n---\n\n## Getting started\n\n```js\nimport { GarageClient } from \"4brains-storage\"; // ESM\n// const { GarageClient } = require('4brains-storage'); // CommonJS\n\nconst storage = new GarageClient({\n  endpoint: process.env.STORAGE_ENDPOINT, // protocol + host + port, no path\n  region: process.env.STORAGE_REGION, // must match your server's region\n  accessKeyId: process.env.STORAGE_KEY_ID,\n  secretAccessKey: process.env.STORAGE_SECRET,\n});\n```\n\n### Constructor options\n\n| Option            | Type   | Default    | Description                                                              |\n| ----------------- | ------ | ---------- | ------------------------------------------------------------------------ |\n| `endpoint`        | string | _required_ | Server origin — protocol, host and port only (no path or query)          |\n| `region`          | string | `'garage'` | Must match the server's configured region; it is part of every signature |\n| `accessKeyId`     | string | _required_ | Access key ID                                                            |\n| `secretAccessKey` | string | _required_ | Secret key — keep it in env vars, never in code                          |\n| `timeoutMs`       | number | `30000`    | Socket inactivity timeout in ms; `0` disables                            |\n\n---\n\n## Buckets\n\n### List buckets\n\n```js\nconst buckets = await storage.listBuckets();\n// [ { name: 'my-bucket', creationDate: '2026-01-01T00:00:00.000Z' }, … ]\n```\n\n### Create a bucket\n\n```js\nawait storage.createBucket(\"my-bucket\");\n// → { created: 'my-bucket' }\n```\n\n> On Garage, buckets created this way get a **key-local alias** — visible only to the\n> key that created them. If the bucket must be visible to other keys or served\n> publicly, create it through your server's admin console/API instead.\n\n### Check whether a bucket exists\n\n```js\nif (await storage.bucketExists(\"my-bucket\")) {\n  /* … */\n}\n```\n\n### Delete a bucket\n\n```js\nawait storage.deleteBucket(\"my-bucket\"); // must be empty first\n// → { deleted: 'my-bucket' }\n```\n\n---\n\n## Upload files\n\nAlways pass `contentType` — the server stores exactly what you send and never infers it\nfrom the file extension. Without it, objects are stored as `application/octet-stream`\nand browsers download them instead of displaying them.\n\n### From a string\n\n```js\nawait storage.putObject(\"my-bucket\", \"notes/hello.txt\", \"hello world\", {\n  contentType: \"text/plain\",\n});\n// → { etag: '\"9a0364b9e99bb480dd25e1f0284c8555\"' }\n```\n\n### From a Buffer\n\n```js\nawait storage.putObject(\"my-bucket\", \"images/logo.png\", imageBuffer, {\n  contentType: \"image/png\",\n});\n```\n\n### From a file on disk (streamed — no memory blow-up)\n\n```js\nimport fs from \"fs\";\n\nawait storage.putObject(\n  \"my-bucket\",\n  \"videos/clip.mp4\",\n  fs.createReadStream(\"clip.mp4\"),\n  {\n    contentType: \"video/mp4\",\n    contentLength: fs.statSync(\"clip.mp4\").size, // required for streams\n  },\n);\n```\n\n> `contentLength` is **required** when the body is a stream. Buffers and strings are\n> measured automatically.\n\n### Update / overwrite an existing file\n\nThere is no separate update call — uploading to the same key replaces the object:\n\n```js\nawait storage.putObject(\"my-bucket\", \"notes/hello.txt\", \"updated content\", {\n  contentType: \"text/plain\",\n});\n```\n\n> There is no versioning: the previous content is gone. Use unique keys (UUIDs) if you\n> need history.\n\n---\n\n## Download files\n\n### As a Buffer (small files)\n\n```js\nconst buf = await storage.getObject(\"my-bucket\", \"notes/hello.txt\");\nconsole.log(buf.toString());\n```\n\n### As a stream (large files)\n\n```js\nconst stream = await storage.getObjectStream(\"my-bucket\", \"videos/clip.mp4\");\nstream.pipe(fs.createWriteStream(\"clip.mp4\"));\n\n// stream.headers holds content-type, content-length, etag, last-modified\n```\n\n### Metadata only (no download)\n\n```js\nconst info = await storage.headObject(\"my-bucket\", \"videos/clip.mp4\");\n// { size: 10485760, contentType: 'video/mp4', etag: '\"…\"', lastModified: '…' }\n```\n\n---\n\n## List files\n\n### Everything under a prefix\n\n```js\nconst { objects, truncated, nextContinuationToken } = await storage.listObjects(\n  \"my-bucket\",\n  { prefix: \"notes/\" },\n);\n\nfor (const o of objects) {\n  console.log(o.key, o.size, o.lastModified, o.etag);\n}\n```\n\n### Folder-style browsing (one level at a time)\n\n```js\nconst { objects, prefixes } = await storage.listObjects(\"my-bucket\", {\n  prefix: \"photos/\",\n  delimiter: \"/\",\n});\n// prefixes → \"subfolders\"; objects → files directly inside photos/\n```\n\n### Pagination\n\n```js\nlet token,\n  all = [];\ndo {\n  const page = await storage.listObjects(\"my-bucket\", {\n    maxKeys: 1000,\n    continuationToken: token,\n  });\n  all.push(...page.objects);\n  token = page.nextContinuationToken;\n} while (token);\n```\n\n### `listObjects` options\n\n| Option              | Description                                                              |\n| ------------------- | ------------------------------------------------------------------------ |\n| `prefix`            | Only keys starting with this string                                      |\n| `delimiter`         | Group keys sharing a prefix up to this character (use `'/'` for folders) |\n| `maxKeys`           | Maximum keys per page                                                    |\n| `continuationToken` | Token from a previous page's `nextContinuationToken`                     |\n\n---\n\n## Copy, rename and delete\n\n### Copy (server-side — no download/upload round-trip)\n\n```js\nawait storage.copyObject(\"my-bucket\", \"a.jpg\", \"my-bucket\", \"backup/a.jpg\");\nawait storage.copyObject(\"source-bucket\", \"a.jpg\", \"target-bucket\", \"a.jpg\");\n```\n\n### Rename / move (copy, then delete the original)\n\n```js\nawait storage.copyObject(\n  \"my-bucket\",\n  \"old-name.jpg\",\n  \"my-bucket\",\n  \"new-name.jpg\",\n);\nawait storage.deleteObject(\"my-bucket\", \"old-name.jpg\");\n```\n\n### Delete one file\n\n```js\nawait storage.deleteObject(\"my-bucket\", \"notes/hello.txt\");\n// → { deleted: 'notes/hello.txt' }\n```\n\n### Delete many files in one request\n\n```js\nawait storage.deleteObjects(\"my-bucket\", [\"a.txt\", \"b.txt\", \"c.txt\"]);\n// → { deleted: [ 'a.txt', 'b.txt', 'c.txt' ] }\n```\n\n---\n\n## Presigned URLs (share links)\n\nGenerate a signed, time-limited link the recipient can open without any credentials —\nworks even on private buckets.\n\n```js\nconst url = storage.presignUrl(\"GET\", \"my-bucket\", \"reports/q3.pdf\", {\n  expiresIn: 3600, // seconds; 1 … 604800 (7 days)\n});\n```\n\n| Option      | Description                                                                                                                                             |\n| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `expiresIn` | Lifetime in seconds; must be between 1 and 604800 (7 days)                                                                                              |\n| `endpoint`  | Sign for a different host than the client's own (the host is part of the signature) — use when the recipient reaches the server through another address |\n\n```js\n// link valid for a host different from the one this client uploads through\nconst url = storage.presignUrl(\"GET\", \"my-bucket\", \"reports/q3.pdf\", {\n  expiresIn: 600,\n  endpoint: process.env.PUBLIC_STORAGE_ENDPOINT,\n});\n```\n\nAnyone holding an unexpired link can read that object — use unguessable keys (UUIDs)\nfor anything sensitive.\n\n---\n\n## Multipart upload (very large files)\n\n```js\nconst uploadId = await storage.createMultipartUpload(\"my-bucket\", \"huge.iso\", {\n  contentType: \"application/octet-stream\",\n});\n\nconst parts = [];\nparts.push(\n  await storage.uploadPart(\"my-bucket\", \"huge.iso\", uploadId, 1, chunk1),\n);\nparts.push(\n  await storage.uploadPart(\"my-bucket\", \"huge.iso\", uploadId, 2, chunk2),\n);\n\nawait storage.completeMultipartUpload(\"my-bucket\", \"huge.iso\", uploadId, parts);\n```\n\nEvery part except the last must be at least 5 MB. If anything fails, release the\npartial upload:\n\n```js\nawait storage.abortMultipartUpload(\"my-bucket\", \"huge.iso\", uploadId);\n```\n\n---\n\n## Error handling\n\nEvery method throws an `Error` carrying `.status` (HTTP status) and `.code` (S3 error\ncode):\n\n```js\ntry {\n  await storage.getObject(\"my-bucket\", \"missing.txt\");\n} catch (err) {\n  if (err.code === \"NoSuchKey\") {\n    // file not found\n  } else if (err.code === \"AccessDenied\") {\n    // wrong credentials/region, or the key lacks permission on this bucket\n  } else if (err.code === \"ETIMEDOUT\") {\n    // server did not respond within timeoutMs\n  } else {\n    throw err;\n  }\n}\n```\n\nCommon codes: `NoSuchKey`, `NoSuchBucket`, `BucketNotEmpty`, `AccessDenied`,\n`SignatureDoesNotMatch`, `ETIMEDOUT`.\n\n---\n\n## Full API reference\n\n### Buckets\n\n| Method                 | Returns                    |\n| ---------------------- | -------------------------- |\n| `listBuckets()`        | `[{ name, creationDate }]` |\n| `createBucket(bucket)` | `{ created }`              |\n| `deleteBucket(bucket)` | `{ deleted }`              |\n| `bucketExists(bucket)` | `boolean`                  |\n\n### Objects\n\n| Method                                                                   | Returns                                                   |\n| ------------------------------------------------------------------------ | --------------------------------------------------------- |\n| `putObject(bucket, key, body, { contentType, contentLength })`           | `{ etag }`                                                |\n| `getObject(bucket, key)`                                                 | `Buffer`                                                  |\n| `getObjectStream(bucket, key)`                                           | readable stream (with `.headers`)                         |\n| `headObject(bucket, key)`                                                | `{ size, contentType, etag, lastModified }`               |\n| `listObjects(bucket, { prefix, delimiter, maxKeys, continuationToken })` | `{ objects, prefixes, truncated, nextContinuationToken }` |\n| `deleteObject(bucket, key)`                                              | `{ deleted }`                                             |\n| `deleteObjects(bucket, keys)`                                            | `{ deleted }`                                             |\n| `copyObject(srcBucket, srcKey, dstBucket, dstKey)`                       | `{ etag }`                                                |\n\n### Multipart\n\n| Method                                                  | Returns                |\n| ------------------------------------------------------- | ---------------------- |\n| `createMultipartUpload(bucket, key, { contentType })`   | `uploadId`             |\n| `uploadPart(bucket, key, uploadId, partNumber, body)`   | `{ partNumber, etag }` |\n| `completeMultipartUpload(bucket, key, uploadId, parts)` | `{ completed }`        |\n| `abortMultipartUpload(bucket, key, uploadId)`           | `{ aborted }`          |\n\n### Links\n\n| Method                                                     | Returns           |\n| ---------------------------------------------------------- | ----------------- |\n| `presignUrl(method, bucket, key, { expiresIn, endpoint })` | signed URL string |\n\n---\n\n## Notes and best practices\n\n1. **Always set `contentType`** on upload — it determines whether browsers display or\n   download the file.\n2. **Keep credentials in environment variables**, never in source control. Backend use\n   only — never ship keys to a browser or mobile app.\n3. **Use unique keys** (UUIDs). Uploading the same key overwrites silently; there is no\n   versioning.\n4. **Sanitize user-supplied filenames** before using them as keys — strip path\n   traversal segments and control characters.\n5. **Stream large files** — `getObjectStream` for downloads, `createReadStream` plus\n   `contentLength` for uploads.\n6. **Region must match the server exactly** — it is part of every signature; a mismatch\n   surfaces as `SignatureDoesNotMatch` or `AccessDenied`.\n\n---\n\n## License\n\nProprietary. © 2026 4Brains Technologies. All rights reserved. Unauthorized copying, distribution, or use is prohibited. contact@4brains.in\n","readmeFilename":"README.md","_rev":"1-bc426446f011041bf9f67a9caa98c686"}