{"_id":"@brashkie/signalis-storage","name":"@brashkie/signalis-storage","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@brashkie/signalis-storage","version":"0.1.0","description":"Decoupled storage layer for Signalis — canonical Signal-Protocol store interfaces plus in-memory and on-disk implementations, generic over crypto types via injected codecs. Zero crypto dependencies.","author":{"name":"Brashkie","url":"Hepein Oficial"},"license":"Apache-2.0","repository":{"type":"git","url":"git+https://github.com/Brashkie/signalis-storage.git"},"homepage":"https://github.com/Brashkie/signalis-storage","bugs":{"url":"https://github.com/Brashkie/signalis-storage/issues"},"keywords":["signal-protocol","signalis","storage","e2e-encryption","session-store","identity-store","prekey-store"],"type":"commonjs","main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"sideEffects":false,"scripts":{"build":"tsup","typecheck":"tsc --noEmit","lint":"biome check src __tests__","lint:fix":"biome check --write src __tests__","format":"biome format --write src __tests__","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run typecheck && npm run lint && npm run test && npm run build"},"devDependencies":{"@biomejs/biome":"^1.9.0","@types/node":"^22.0.0","@vitest/coverage-v8":"^3.0.0","tsup":"^8.0.0","typescript":"^6.0.3","vitest":"^3.0.0"},"engines":{"node":">=18"},"_id":"@brashkie/signalis-storage@0.1.0","gitHead":"6b835fb4191e6bdc0cd45747fc55d21cd39436dc","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-pHlWhtnV/tpf2YLvJjtyiyvyT5NJx+aY6Qp8vBD/sg0wCPF95x/5dIQ4m0IB/y/OjnBkASG74ZNgYaB72d+Anw==","shasum":"7af15d1134271b178f7ed1b9e0749a866261038f","tarball":"https://registry.npmjs.org/@brashkie/signalis-storage/-/signalis-storage-0.1.0.tgz","fileCount":10,"unpackedSize":217737,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@brashkie%2fsignalis-storage@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIADU0uaDJ+IjThWDqxOaXIQC1pmxMjpd8WNLqyOOl73SAiAW7q2d5rxue61fPVd1N67ZlbT6CD9v16C3Wc66vsMl7g=="}]},"_npmUser":{"name":"brashkie","email":"fabianoarjunken@gmail.com"},"directories":{},"maintainers":[{"name":"brashkie","email":"fabianoarjunken@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/signalis-storage_0.1.0_1784142199608_0.05463107728925998"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-15T19:03:19.476Z","0.1.0":"2026-07-15T19:03:19.747Z","modified":"2026-07-15T19:03:20.093Z"},"maintainers":[{"name":"brashkie","email":"fabianoarjunken@gmail.com"}],"description":"Decoupled storage layer for Signalis — canonical Signal-Protocol store interfaces plus in-memory and on-disk implementations, generic over crypto types via injected codecs. Zero crypto dependencies.","homepage":"https://github.com/Brashkie/signalis-storage","keywords":["signal-protocol","signalis","storage","e2e-encryption","session-store","identity-store","prekey-store"],"repository":{"type":"git","url":"git+https://github.com/Brashkie/signalis-storage.git"},"author":{"name":"Brashkie","url":"Hepein Oficial"},"bugs":{"url":"https://github.com/Brashkie/signalis-storage/issues"},"license":"Apache-2.0","readme":"<div align=\"center\">\n\n# @brashkie/signalis-storage\n\n**The decoupled storage layer for the [Signalis](https://github.com/Brashkie/signalis) Signal-Protocol implementation.**\n\n[![CI](https://github.com/Brashkie/signalis-storage/actions/workflows/ci.yml/badge.svg)](https://github.com/Brashkie/signalis-storage/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/@brashkie/signalis-storage.svg)](https://www.npmjs.com/package/@brashkie/signalis-storage)\n[![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)\n[![types](https://img.shields.io/badge/types-TypeScript-3178c6.svg)](https://www.typescriptlang.org/)\n[![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](#testing--coverage)\n[![node](https://img.shields.io/badge/node-%3E%3D18-339933.svg)](https://nodejs.org/)\n\n[English](./README.md) · [Español](./README.es.md)\n\n</div>\n\n---\n\nCanonical Signal-Protocol store interfaces (`IdentityStore`, `PreKeyStore`, `SignedPreKeyStore`, `SessionStore`), the `ProtocolAddress` primitive, atomic file helpers, and two production-ready implementations — **in-memory** and **on-disk** — all **generic** over the crypto object types and wired through injected codecs.\n\nThis package carries **zero dependency on the crypto core.** The cryptographic classes (`IdentityKeyPair`, `Session`, and friends) are supplied by the consumer through small codec objects. The result is an acyclic dependency graph and a storage layer you can test, version, and reuse independently of the protocol.\n\n```\n  @brashkie/signalis  ──depends on──▶  @brashkie/signalis-storage\n        │                                       ▲\n        └── injects codecs ─────────────────────┘   (no reverse import)\n```\n\n---\n\n## Table of Contents\n\n- [Who is this for?](#who-is-this-for)\n- [Installation](#installation)\n- [Quick start](#quick-start)\n- [Architecture: why codecs?](#architecture-why-codecs)\n- [API reference](#api-reference)\n- [Writing a custom adapter](#writing-a-custom-adapter)\n- [On-disk layout](#on-disk-layout)\n- [Durability & atomic writes](#durability--atomic-writes)\n- [Security considerations](#security-considerations)\n- [Testing & coverage](#testing--coverage)\n- [Ecosystem](#ecosystem)\n- [Versioning & stability](#versioning--stability)\n- [License](#license)\n\n---\n\n## Who is this for?\n\n**Most people should not install this package directly.** `@brashkie/signalis` re-exports pre-wired versions of every store, so application code gets `new FileIdentityStore(dir)` with no codecs to think about.\n\nInstall `@brashkie/signalis-storage` directly if you are:\n\n- **Building a custom storage adapter** — SQLite, Redis, IndexedDB, Postgres, Strenor, or any other backend.\n- **Writing tooling** that inspects or migrates Signalis on-disk state.\n- **Auditing** the storage layer in isolation from the crypto core.\n\n---\n\n## Installation\n\n```bash\nnpm install @brashkie/signalis-storage\n# or\npnpm add @brashkie/signalis-storage\n# or\nyarn add @brashkie/signalis-storage\n```\n\nShips dual **CommonJS + ESM** with full TypeScript declarations. Node.js **≥ 18**.\n\n---\n\n## Quick start\n\nUsing the bundled file store to build a custom adapter (the crypto codec is what\n`@brashkie/signalis` normally provides for you):\n\n```ts\nimport {\n  FileSessionStore,\n  ProtocolAddress,\n  type Codec,\n} from '@brashkie/signalis-storage';\n\n// A codec bridges your crypto object <-> a JSON-friendly snapshot.\nconst sessionCodec: Codec<MySession> = {\n  serialize: (s) => s.serialize(),\n  deserialize: (snap) => MySession.deserialize(snap),\n};\n\nconst store = new FileSessionStore('/var/lib/myapp/signal', sessionCodec);\n\nawait store.saveSession(new ProtocolAddress('alice@example.com', 1), session);\nconst restored = await store.loadSession(new ProtocolAddress('alice@example.com', 1));\n```\n\n---\n\n## Architecture: why codecs?\n\nThe store implementations must serialize crypto objects to durable storage and\nreconstruct them later — e.g. `IdentityKeyPair.deserialize(bytes)`. Those classes\nlive in `@brashkie/signalis`. If storage imported them directly while `signalis`\nimported storage, the result would be a **circular dependency**.\n\nThe resolution: storage never names a crypto class. Each store that needs\nserialization receives a **codec**, and each store that needs to compare public\nkeys receives a **fingerprint**:\n\n```ts\ninterface Codec<T> {\n  serialize(value: T): unknown;      // → a JSON-serializable snapshot\n  deserialize(snapshot: unknown): T; // ← reconstruct the object\n}\n\ninterface Fingerprint<T> {\n  fingerprint(value: T): string;     // a stable, comparable string\n}\n```\n\n`@brashkie/signalis` supplies the concrete codecs exactly once, wraps the generic\nstores in thin subclasses, and re-exports them. Application authors never see this\nmachinery — they instantiate `new FileIdentityStore(dir)` and it just works.\n\n**Design consequences:**\n\n| Property | Benefit |\n|----------|---------|\n| Zero crypto import | Acyclic dependency graph; storage builds & tests standalone |\n| Generic over `T` | One implementation serves every crypto shape |\n| Codec at the boundary | At-rest encryption, compression, or format changes are localized |\n| Interfaces are the contract | Any backend (SQL, KV, cloud) is a drop-in |\n\n---\n\n## API reference\n\n### Address\n\n| Export | Description |\n|--------|-------------|\n| `ProtocolAddress` | `(userId, deviceId)` peer identifier; filesystem-safe, immutable |\n| `isProtocolAddress(v)` | Type guard |\n| `MAX_DEVICE_ID`, `MAX_USER_ID_LENGTH` | Validation bounds |\n\n```ts\nconst addr = new ProtocolAddress('alice@example.com', 1);\naddr.toString();          // \"alice@example.com.1\"\nProtocolAddress.parse('alice@example.com.1'); // round-trips\naddr.equals(other);       // structural equality\n```\n\n### Store interfaces (generic)\n\n| Interface | Type params | Responsibility |\n|-----------|-------------|----------------|\n| `IdentityStore<KP, PK>` | key pair, public key | own identity + registration id + trusted-peer fingerprints (TOFU) |\n| `PreKeyStore<OPK>` | one-time prekey | one-time prekeys (delete-after-use) |\n| `SignedPreKeyStore<SPK>` | signed prekey | signed prekeys + active pointer |\n| `SessionStore<S>` | session | per-peer Double Ratchet state |\n\n### Codec contracts\n\n| Export | Description |\n|--------|-------------|\n| `Codec<T>` | `serialize` / `deserialize` round-trip |\n| `Fingerprint<T>` | one-way stable string for comparison |\n\n### Bundled implementations\n\n| Class | Backend | Needs |\n|-------|---------|-------|\n| `MemoryIdentityStore<KP, PK>` | in-process Map | `Fingerprint<PK>` |\n| `MemoryPreKeyStore<OPK>` | in-process Map | — |\n| `MemorySignedPreKeyStore<SPK>` | in-process Map | — |\n| `MemorySessionStore<S>` | in-process Map | `Codec<S>` |\n| `FileIdentityStore<KP, PK>` | JSON on disk | `Codec<KP>` + `Fingerprint<PK>` |\n| `FilePreKeyStore<OPK>` | JSON on disk | `Codec<OPK>` |\n| `FileSignedPreKeyStore<SPK>` | JSON on disk | `Codec<SPK>` |\n| `FileSessionStore<S>` | JSON on disk | `Codec<S>` |\n\n### File helpers\n\n`atomicWriteFile`, `readFileOrNull`, `unlinkIfExists`, `listFiles` — exposed for\nauthors of custom file-based adapters.\n\n### Errors\n\n`StorageError` (base) · `StorageValidationError` · `SerializationError`. All carry\nan optional structured `context` bag.\n\n---\n\n## Writing a custom adapter\n\nImplement the interface you need. Everything is generic over your crypto types, so\nyour adapter never hard-codes a crypto class:\n\n```ts\nimport type { SessionStore, Codec } from '@brashkie/signalis-storage';\nimport { ProtocolAddress } from '@brashkie/signalis-storage';\n\nexport class RedisSessionStore<S> implements SessionStore<S> {\n  constructor(\n    private readonly redis: RedisClient,\n    private readonly codec: Codec<S>,\n  ) {}\n\n  async saveSession(address: ProtocolAddress, session: S): Promise<void> {\n    const snapshot = this.codec.serialize(session);\n    await this.redis.set(`sess:${address.toString()}`, JSON.stringify(snapshot));\n  }\n\n  async loadSession(address: ProtocolAddress): Promise<S | null> {\n    const raw = await this.redis.get(`sess:${address.toString()}`);\n    return raw === null ? null : this.codec.deserialize(JSON.parse(raw));\n  }\n\n  async containsSession(address: ProtocolAddress): Promise<boolean> {\n    return (await this.redis.exists(`sess:${address.toString()}`)) === 1;\n  }\n\n  async deleteSession(address: ProtocolAddress): Promise<void> {\n    await this.redis.del(`sess:${address.toString()}`);\n  }\n\n  async loadAllSessions(): Promise<Array<{ address: ProtocolAddress; session: S }>> {\n    const keys = await this.redis.keys('sess:*');\n    const out: Array<{ address: ProtocolAddress; session: S }> = [];\n    for (const key of keys) {\n      const raw = await this.redis.get(key);\n      if (raw === null) continue;\n      out.push({\n        address: ProtocolAddress.parse(key.slice('sess:'.length)),\n        session: this.codec.deserialize(JSON.parse(raw)),\n      });\n    }\n    return out;\n  }\n}\n```\n\nThe consumer (usually `@brashkie/signalis`) wires the concrete codec:\n\n```ts\nimport { Session } from '@brashkie/signalis';\n\nconst sessionCodec = {\n  serialize: (s: Session) => s.serialize(),\n  deserialize: (snap: unknown) => Session.deserialize(snap as never),\n};\n\nconst store = new RedisSessionStore(redis, sessionCodec);\n```\n\n---\n\n## On-disk layout\n\nThe `File*` stores use a predictable, human-inspectable JSON layout:\n\n```\n<rootDir>/\n├── identity.json              # own keypair — sensitive (written chmod 600)\n├── registration-id.json\n├── trusted/\n│   └── <address>.json         # one file per peer (TOFU fingerprints)\n├── prekeys/\n│   └── <id>.json\n├── signed-prekeys/\n│   ├── <id>.json\n│   └── active.json            # pointer to the active signed prekey id\n└── sessions/\n    └── <address>.json         # per-peer ratchet state — sensitive\n```\n\nEvery record is a small versioned JSON document (`{ version, ... }`) so future\nformat migrations are detectable.\n\n---\n\n## Durability & atomic writes\n\nSignal key material must never be left half-written after a crash. Every write goes\nthrough `atomicWriteFile`, which performs:\n\n1. Write to `<path>.tmp.<random>`\n2. `fsync()` to flush kernel buffers to the physical device\n3. `rename()` to the final path (atomic on the same filesystem)\n\nAt any instant the file on disk is either the complete old value or the complete\nnew value — never a partial write.\n\n**Windows resilience.** On Windows, the final `rename()` can transiently fail with\n`EPERM` / `EBUSY` / `EACCES` when anti-virus, the search indexer, or another handle\nbriefly holds the target. `atomicWriteFile` retries up to five times with\nexponential backoff (1 → 16 ms) before surfacing the error — the same hardening\npattern used by `esbuild`, `sharp`, and other mature native-adjacent packages.\n\n---\n\n## Security considerations\n\n- **Session and identity files contain sensitive key material.** At-rest\n  encryption is strongly recommended in production. Because the codec sits at the\n  serialization boundary, you can wrap it to encrypt/decrypt transparently, or\n  deploy the file stores onto an encrypted filesystem / secure enclave.\n- **One-time prekeys are single-use.** They MUST be deleted after a successful\n  handshake (`removePreKey`) — reuse breaks forward secrecy. The `SessionBuilder`\n  in `@brashkie/signalis` enforces this.\n- **`ProtocolAddress` is filesystem-hardened.** `userId` values containing path\n  separators, control characters, or shell-meta characters are rejected at\n  construction, preventing path traversal in the file stores.\n- **No secrets in logs.** Error `context` bags never include key material — only\n  ids and addresses.\n\nReport vulnerabilities per [SECURITY.md](./SECURITY.md).\n\n---\n\n## Testing & coverage\n\n```bash\nnpm test            # run the suite\nnpm run test:coverage\n```\n\nThe suite exercises every store (memory + file), the codec-injection boundary,\n`ProtocolAddress` validation, atomic-write error paths (including simulated\nWindows `EPERM` retries), and the error hierarchy.\n\n| Metric | Coverage |\n|--------|----------|\n| Statements | 100% |\n| Branches | ~98% |\n| Functions | 100% |\n\nThe small remaining uncovered lines are defensive dead-code (nested `catch`\ncleanup blocks) that cannot be triggered without corrupting the test sandbox.\n\n---\n\n## Ecosystem\n\n| Package | Role |\n|---------|------|\n| [`@brashkie/signalis`](https://github.com/Brashkie/signalis) | The protocol: X3DH, Double Ratchet, Sender Keys, sessions |\n| [`@brashkie/signalis-core`](https://github.com/Brashkie/signalis-core) | Native Rust cryptographic primitives (Curve25519, HKDF, AEAD…) |\n| **`@brashkie/signalis-storage`** | **This package — the storage contract + Memory/File impls** |\n| `@brashkie/signalis-storage-strenor` *(planned)* | Adapter over [Strenor](https://github.com/Brashkie/strenor), an embedded KV engine |\n| `@brashkie/signalis-storage-sqlite` *(on demand)* | Adapter over `better-sqlite3` |\n| `@brashkie/signalis-storage-redis` *(on demand)* | Adapter over the Redis client |\n\nSee [ROADMAP.md](./ROADMAP.md) for the storage roadmap.\n\n---\n\n## Versioning & stability\n\nSemantic Versioning. Pre-1.0, minor versions may include interface refinements —\npin a caret range (`^0.x`) and read the [CHANGELOG](./CHANGELOG.md) before\nupgrading. The store interfaces are the stability surface; bundled implementations\nmay gain diagnostics without a breaking bump.\n\n---\n\n## License\n\nApache-2.0 © Brashkie (Hepein Oficial)\n","readmeFilename":"README.md","_rev":"1-576c8f6d58d213679e037616db171326"}