Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
# @suaveplan/drive-sharing - Partitioned drive sharing engine for the Suaveplan ecosystem: envelope encryption with mandatory AAD, per-user key wrapping, partition policies with deterministic action validation, revocable share tokens, expiring grants, and atomic cross-partition file transfer with rollback.
## Summary
Partitioned drive sharing engine for the Suaveplan ecosystem. Ships envelope encryption with mandatory AAD, per-user key wrapping, partition policies with deterministic action validation, revocable share tokens, expiring grants, and atomic cross-partition file transfer with rollback. Every public boundary is Zod-validated, OTel-instrumented, and event-bus-aware.
<!-- begin:auto-docs -->
## Documentation tree
- [envelope](./src/encryption/envelope/envelope.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [envelope](./src/encryption/envelope/envelope.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module is the single source of truth for every failure category emitted by `@suaveplan/drive-sharing`…
- [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module is the single source of truth for every failure category emitted by `@suaveplan/drive-sharing`…
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [common](./src/types/common.md) — This module declares the foundational types shared by every other module in `@suaveplan/drive-sharing`. It owns the bra…
- [schemas](./src/types/schemas.md) — This module declares Zod schemas for every public option type accepted by `@suaveplan/drive-sharing`. Schemas are wired…
<!-- end:auto-docs --> - [envelope](./src/encryption/envelope/envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module declares the foundational types shared by every other module in `@suaveplan/drive-sharing`. It owns the bra…
- [schemas](./src/types/schemas.md) — This module declares Zod schemas for every public option type accepted by `@suaveplan/drive-sharing`. Schemas are wired…
- [error-codes](./src/error-codes.md) — The `error-codes` module is the single source of truth for every failure category emitted by `@suaveplan/drive-sharing`…
- [envelope](./src/encryption/envelope/envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module declares the foundational types shared by every other module in `@suaveplan/drive-sharing`. It owns the bra…
- [schemas](./src/types/schemas.md) — This module declares Zod schemas for every public option type accepted by `@suaveplan/drive-sharing`. Schemas are wired…
- [error-codes](./src/error-codes.md) — The `error-codes` module is the single source of truth for every failure category emitted by `@suaveplan/drive-sharing`…
- [envelope](./src/encryption/envelope/envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module declares the foundational types shared by every other module in `@suaveplan/drive-sharing`. It owns the bra…
- [schemas](./src/types/schemas.md) — This module declares Zod schemas for every public option type accepted by `@suaveplan/drive-sharing`. Schemas are wired…
- [error-codes](./src/error-codes.md) — The `error-codes` module is the single source of truth for every failure category emitted by `@suaveplan/drive-sharing`…
- [envelope](./src/encryption/envelope/envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_ - [key-ring](./src/encryption/key-ring/key-ring.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ - [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ - [partition-manager](./src/partitions/partition-manager/partition-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_ - [partition-policy](./src/partitions/partition-policy/partition-policy.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_ - [partition-store](./src/partitions/partition-store/partition-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_ - [grants](./src/sharing/grants/grants.md) — <!-- begin:auto-docs --> _(no submodules)_- [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tokens](./src/sharing/tokens/tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [transfer](./src/transfer/transfer/transfer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module declares the foundational types shared by every other module in `@suaveplan/drive-sharing`. It owns the bra…
- [schemas](./src/types/schemas.md) — This module declares Zod schemas for every public option type accepted by `@suaveplan/drive-sharing`. Schemas are wired…
- [error-codes](./src/error-codes.md) — The `error-codes` module is the single source of truth for every failure category emitted by `@suaveplan/drive-sharing`…
## Install
```bash
bun add @suaveplan/drive-sharing
```
## Quick start
```ts
import {
GrantManager,
InMemoryPartitionStore,
KeyRing,
PartitionManager,
TokenManager,
TransferManager,
envelopeEncrypt,
envelopeDecrypt,
fileId,
partitionId,
userId,
wrapKey,
} from "@suaveplan/drive-sharing";
const keyRing = new KeyRing();
const partitions = new PartitionManager({
store: new InMemoryPartitionStore(),
keyRing,
});
const partition = await partitions.create({
name: "research",
type: "shared",
ownerId: userId("alice"),
});
const grants = new GrantManager();
const tokens = new TokenManager();
```
## Modules
| Subpath | Purpose |
|---|---|
| `@suaveplan/drive-sharing` | Default barrel — re-exports every public type and class. |
| `@suaveplan/drive-sharing/errors` | `DriveShareError`, `DRIVE_SHARE_CODES`. |
| `@suaveplan/drive-sharing/constants` | TTL defaults, format version, scalability caps. |
| `@suaveplan/drive-sharing/types/common` | Branded IDs, wire types, `Result`, `generateId`. |
| `@suaveplan/drive-sharing/types/schemas` | Zod schemas for every public option type. |
| `@suaveplan/drive-sharing/encryption/envelope` | `envelopeEncrypt`, `envelopeDecrypt`, `wrapKey`, `unwrapKey`, `rewrapKey`, `WebCryptoProvider`. |
| `@suaveplan/drive-sharing/encryption/key-ring` | `KeyRing` with add / get / remove / rotate / export / import. |
| `@suaveplan/drive-sharing/partitions/partition-policy` | `createPolicy`, `validateAction`, `DEFAULT_PRESETS`. |
| `@suaveplan/drive-sharing/partitions/partition-store` | `PartitionStore` interface, `InMemoryPartitionStore`. |
| `@suaveplan/drive-sharing/partitions/partition-manager` | `PartitionManager` lifecycle. |
| `@suaveplan/drive-sharing/sharing/grants` | `GrantManager` (in-memory grants with check / revoke / list). |
| `@suaveplan/drive-sharing/sharing/tokens` | `TokenManager` (HMAC-SHA-256 integrity-tagged tokens). |
| `@suaveplan/drive-sharing/transfer` | `TransferManager` (cross-partition rewrap with `AbortSignal`). |
| `@suaveplan/drive-sharing/otel` | OTel instrument singletons. |
## Security model
- **AAD is mandatory** — every `envelopeEncrypt` and `wrapKey` call requires a non-empty `aad: Uint8Array`. Empty AAD throws `SHARE_ENCRYPTION_FAILED` to enforce the partition-binding guarantee.
- **No plaintext keys leave the package** — `KeyRing.rotateKey` zeroizes its caller-supplied buffer; `rewrapKey` zeroizes the intermediate plaintext DEK before returning.
- **HMAC-SHA-256 token integrity** — every share token carries an integrity tag computed under the partition master key. Tampering invalidates the tag.
- **No key material in events / spans / logs** — every event payload carries IDs only.
## Performance baselines
| Benchmark | Iteration | Wall-clock |
|---|---|---|
| `grant_check_10k` | 10 000 in-memory grant checks | < 50 ms |
| `token_validate_10k` | 10 000 in-memory token validations | < 50 ms |
| `envelope_encrypt_1mb` | one 1 MiB AES-GCM encrypt | < 10 ms |
| `envelope_decrypt_1mb` | one 1 MiB AES-GCM decrypt | < 10 ms |
| `keyring_rotate_1000` | 1 000 rotateKey calls | < 250 ms |
Run benchmarks with `bun run src/__benchmarks__/drive-sharing.bench.ts`.
## Testing posture
- 100% statement / branch / function / line coverage (Istanbul).
- Property-based tests via `fast-check` covering grant expiry, token revocation finality, policy merge commutativity, and partition-member invariants.
- All test files import runner primitives from `@suaveplan/testing/runner` for dual `vitest` + `bun:test` portability.
## License
Proprietary — Suaveplan commercial license.

View File

@@ -0,0 +1,105 @@
# `encryption/envelope/envelope.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module is the cryptographic core of `@suaveplan/drive-sharing`. It declares the `CryptoProvider` interface, ships the default `WebCryptoProvider` implementation backed by noble AEAD primitives, and exposes the public envelope-encryption API: `envelopeEncrypt`, `envelopeDecrypt`, `wrapKey`, `unwrapKey`, `rewrapKey`, `computeIntegrity`, `verifyIntegrity`. Every function here is the only call site that touches AEAD ciphers; downstream modules (key-ring, transfer, partition-manager) consume only this surface so adding a new algorithm or swapping in a FIPS-validated provider is a single-file change.
## Features
- Mandatory non-empty AAD on every encrypt and unwrap operation. Empty AAD produces `SHARE_ENCRYPTION_FAILED`, foreclosing cut-and-paste attacks across partitions.
- `WrappedKey` carries a SHA-256 checksum verified before unwrap; corrupt entries throw `SHARE_KEY_NOT_FOUND` instead of silently passing.
- Configurable AEAD algorithm (`AES-GCM` default, `XChaCha20-Poly1305` opt-in) selected per call.
- `rewrapKey` zeroizes the intermediate plaintext key via `@suaveplan/crypto-utils`'s `zeroize`, so an attacker who reads heap memory after a rotation cannot recover the DEK.
- `verifyIntegrity` is timing-safe via `constantTimeEqual`.
- OTel instrumentation: every public function records `share.envelope.duration_ms` and starts a `share.envelope.encrypt` / `share.envelope.decrypt` span; no key material or plaintext bytes are exported as attributes.
## Usage: Basic Example
```ts
import {
envelopeEncrypt,
envelopeDecrypt,
} from "@suaveplan/drive-sharing/encryption/envelope";
const key = new Uint8Array(32).fill(0x42);
const aad = new TextEncoder().encode("partition-1::file-7");
const data = new TextEncoder().encode("secret payload");
const bundle = await envelopeEncrypt(key, data, { aad });
const plaintext = await envelopeDecrypt(key, bundle);
console.log(new TextDecoder().decode(plaintext)); // "secret payload"
```
## Usage: Advanced Example
```ts
import {
envelopeEncrypt,
rewrapKey,
wrapKey,
unwrapKey,
} from "@suaveplan/drive-sharing/encryption/envelope";
// Cross-partition key migration without exposing plaintext.
const oldKEK = new Uint8Array(32).fill(0xaa);
const newKEK = new Uint8Array(32).fill(0xbb);
const dek = new Uint8Array(32).fill(0xcc);
const aad = new TextEncoder().encode("partition-1::user-alice");
const wrapped = await wrapKey(oldKEK, dek, { aad });
const rewrapped = await rewrapKey(oldKEK, newKEK, wrapped, { aad });
const recovered = await unwrapKey(newKEK, rewrapped, { aad });
// recovered === dek (by value)
```
## API Reference
### `interface CryptoProvider`
Pluggable provider interface. Every method is async; AAD is mandatory on `encrypt`, `wrapKey`, and `unwrapKey`. The default implementation is `WebCryptoProvider`.
### `class WebCryptoProvider implements CryptoProvider`
Default provider. Uses `aes256gcm` / `xchacha20poly1305` from `@suaveplan/crypto-cipher`, draws nonces from `randomBytes` in `@suaveplan/crypto-utils`, computes integrity via `sha256` in `@suaveplan/crypto-hash`. AES-256-GCM is the default algorithm; `XChaCha20-Poly1305` is opt-in via the `algorithm` option.
### `async function envelopeEncrypt(key, data, options): Promise<EncryptedBundle>`
- `key: Uint8Array` — 32-byte AEAD key.
- `data: Uint8Array` — plaintext.
- `options.aad: Uint8Array` — required, non-empty.
- `options.algorithm?: EncryptionAlgorithm` — defaults to `AES-GCM`.
- **Throws** `DriveShareError(SHARE_ENCRYPTION_FAILED)` on any failure.
### `async function envelopeDecrypt(key, bundle): Promise<Uint8Array>`
Decrypts. Mismatched AAD or tampered ciphertext throws `SHARE_ENCRYPTION_FAILED`.
### `async function wrapKey(wrappingKey, keyToWrap, options): Promise<WrappedKey>`
Wraps `keyToWrap` with `wrappingKey`. The result carries `algorithm`, `data`, and a SHA-256 hex `checksum`.
### `async function unwrapKey(wrappingKey, wrappedKey, options): Promise<Uint8Array>`
Validates checksum then decrypts. Throws `SHARE_KEY_NOT_FOUND` on checksum mismatch and `SHARE_ENCRYPTION_FAILED` on AEAD failure.
### `async function rewrapKey(oldWrappingKey, newWrappingKey, wrappedKey, options): Promise<WrappedKey>`
Atomic rewrap. Zeroizes the intermediate plaintext key before returning.
### `async function computeIntegrity(data): Promise<string>`
SHA-256 hex digest.
### `async function verifyIntegrity(data, expected): Promise<boolean>`
Constant-time comparison.
## Implementation Notes
- The bundle stores AAD in-band (`bundle.aad`) so `envelopeDecrypt` is callable with only `(key, bundle)` per the spec scenario. The AAD is also fed to the AEAD primitive at decrypt time, so any tampering of the in-band AAD invalidates the tag.
- AES-KW is accepted as an `EncryptionAlgorithm` tag for backward compatibility but internally uses AES-256-GCM with the AAD bound to the partition || user context — this is functionally equivalent and avoids depending on an extra primitive.
- The SHA-256 `checksum` field on `WrappedKey` is **not** a substitute for the AEAD tag — it is a fast sanity check so the unwrap function can reject obviously-corrupt data without paying the cost of an AEAD attempt.
- Nonces are drawn fresh on every encrypt call via `randomBytes`. Because this routes through `crypto.getRandomValues`, the collision probability for a 96-bit GCM nonce is ~2⁻⁴⁸ per ~2³² keys — well below the IETF's 2⁻³² safety bound.

View File

@@ -0,0 +1,90 @@
# `encryption/key-ring/key-ring.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`KeyRing` is the multi-user wrapped-key store at the heart of `@suaveplan/drive-sharing`. It owns the partition master key, holds per-user data keys (DEKs) wrapped under that master key, and exposes the lifecycle operations needed to add, retrieve, rotate, remove, export, and import wrapped entries. Every operation emits a lifecycle event so callers can persist to durable storage and audit changes; events deliberately carry only `userId` (no key material).
## Features
- AAD-bound wrap / unwrap via `wrapKey` / `unwrapKey` from `encryption/envelope`. The AAD is per-user (`drive-sharing/v1/keyring/<userId>`) so a wrapped key cannot be moved between users without producing an AEAD failure.
- `rotateKey` rewraps a single user's entry; `rotateMasterKey` rewraps every entry atomically. Both zeroize old buffers via `@suaveplan/crypto-utils`'s `zeroize`.
- Versioned JSON-like `export` / `import`. The export carries `keyring_format_version: 1`; unknown versions throw `SHARE_KEYRING_FORMAT_UNSUPPORTED`.
- Hard-cap on entries (`KEYRING_MAX_ENTRIES = 100_000`) prevents pathological bulk-add denial-of-service.
- Event bus (`key:added`, `key:removed`, `key:rotated`) with `on(event, listener) => unsubscribe`.
## Usage: Basic Example
```ts
import { KeyRing } from "@suaveplan/drive-sharing/encryption/key-ring";
import { userId } from "@suaveplan/drive-sharing/types/common";
const ring = new KeyRing();
const dek = new Uint8Array(32).fill(0xab);
await ring.addKey(userId("alice"), dek);
const recovered = await ring.getKey(userId("alice"));
```
## Usage: Advanced Example
```ts
import { KeyRing } from "@suaveplan/drive-sharing/encryption/key-ring";
import { userId } from "@suaveplan/drive-sharing/types/common";
const masterKey = new Uint8Array(32).fill(0x01);
const ring = new KeyRing({ masterKey });
const off = ring.on("key:rotated", ({ userId }) => {
console.log("rotated:", userId);
});
await ring.addKey(userId("alice"), new Uint8Array(32).fill(0x10));
await ring.rotateKey(userId("alice"), new Uint8Array(32).fill(0x20));
// Backup / restore round-trip.
const blob = ring.export();
const restored = new KeyRing({ masterKey });
await restored.import(blob);
off();
```
## API Reference
### `class KeyRing`
- `constructor(config?: KeyRingConfig)` — `masterKey` defaults to a fresh 32-byte CSPRNG buffer.
- `hasKey(userId): boolean`
- `listKeys(): UserId[]`
- `addKey(userId, key): Promise<void>` — wraps and stores. Emits `key:added`.
- `getKey(userId): Promise<Uint8Array | undefined>`
- `removeKey(userId): Promise<void>` — emits `key:removed` only when an entry is actually removed.
- `rotateKey(userId, newKey): Promise<UserWrappedKey>` — replaces the entry, emits `key:rotated`, increments `share.key.rotate_total`.
- `rotateMasterKey(newMasterKey): Promise<void>` — rewraps every entry atomically.
- `export(): { keyring_format_version: 1; entries: UserWrappedKey[] }`
- `import(payload): Promise<void>` — Zod-validated.
- `on(event, listener): () => void`
### `interface UserWrappedKey`
`{ userId, wrappedKey }`. The export shape stores arrays of these.
### `interface KeyRingConfig`
`{ masterKey?: Uint8Array }`. Master key length must be exactly 32 bytes.
### Events
- `key:added` — `{ userId }`
- `key:removed` — `{ userId }`
- `key:rotated` — `{ userId }`
## Implementation Notes
- The constructor copies the supplied `masterKey` so the caller's buffer can be zeroized independently. `rotateMasterKey` writes the new key into the same buffer slot via `Uint8Array.set` so external references to the slot are invalidated rather than left dangling.
- `export` deliberately produces a plain object (not a JSON string) so the caller can pipe it through their preferred serializer (e.g. `serialize` for tagged JSON). Validation lives in `KeyRingExportSchema`.
- The hard cap `KEYRING_MAX_ENTRIES = 100_000` matches the OpenSpec scalability table. Above this, the spec calls for sharded ring storage which is out of scope for this package.
- `rotateKey` zeroizes the caller-supplied `newKey` on success — this matches the spec's "no plaintext key in caller's heap after success" guarantee. If the caller wants to retain the bytes they should pass a copy.
- The `aadFor` derivation uses ASCII text (`drive-sharing/v1/keyring/<userId>`) rather than raw bytes so the AAD is human-readable in audit logs that capture it.

View File

@@ -0,0 +1,91 @@
# `error-codes.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `error-codes` module is the single source of truth for every failure category emitted by `@suaveplan/drive-sharing`. It registers a frozen, namespaced code list with the global `defineCodes` registry from `@suaveplan/error` and exports the concrete `DriveShareError` subclass that every public code path throws. Stable, machine-readable codes let consumers write exhaustive `switch` statements on caught errors without having to parse human-readable messages. The brand symbol on every `DriveShareError` instance protects callers from imposters (e.g. user-land error classes that copy the field shape but were thrown elsewhere).
## Features
- Frozen literal-typed `DRIVE_SHARE_CODES` registry covering eleven distinct failure reasons across partitions, grants, tokens, keys, encryption, transfer, and key-ring import.
- `DriveShareError` extends `SuaveplanError<DriveShareErrorCode>` so `error.code` narrows to the literal union.
- Brand-symbol stamping plus a static `DriveShareError.is()` guard for cross-realm safety.
- Idempotent eager-registration helper (`registerDriveShareErrorCodes`) so consumers can pre-warm the registry from a startup hook.
- Side-effect call at module bottom guarantees the codes are registered the first time the module is imported.
## Usage: Basic Example
```ts
import {
DriveShareError,
DRIVE_SHARE_CODES,
} from "@suaveplan/drive-sharing/errors";
try {
// ...some drive-sharing call...
} catch (error) {
if (DriveShareError.is(error)) {
if (error.code === DRIVE_SHARE_CODES.SHARE_PARTITION_NOT_FOUND) {
console.warn("partition vanished — falling back to read-only");
}
}
}
```
## Usage: Advanced Example
```ts
import {
DriveShareError,
type DriveShareErrorCode,
} from "@suaveplan/drive-sharing/errors";
// Exhaustive switch — TypeScript will complain if you forget a case.
function classify(error: DriveShareError): "transient" | "permanent" {
const code: DriveShareErrorCode = error.code;
switch (code) {
case "DRIVE_SHARE.SHARE_TRANSFER_FAILED":
case "DRIVE_SHARE.SHARE_POLICY_VIOLATION":
return "transient";
case "DRIVE_SHARE.SHARE_PARTITION_NOT_FOUND":
case "DRIVE_SHARE.SHARE_GRANT_NOT_FOUND":
case "DRIVE_SHARE.SHARE_TOKEN_INVALID":
case "DRIVE_SHARE.SHARE_TOKEN_EXPIRED":
case "DRIVE_SHARE.SHARE_TOKEN_REVOKED":
case "DRIVE_SHARE.SHARE_KEY_NOT_FOUND":
case "DRIVE_SHARE.SHARE_ENCRYPTION_FAILED":
case "DRIVE_SHARE.SHARE_KEYRING_FORMAT_UNSUPPORTED":
case "DRIVE_SHARE.SHARE_INTEGRITY_VERIFICATION_FAILED":
return "permanent";
}
}
```
## API Reference
### `const DRIVE_SHARE_CODES`
Frozen `Record<DriveShareErrorCode, DriveShareErrorCode>` returned by `defineCodes`. Iterating the values yields every fully-qualified code string.
### `type DriveShareErrorCode`
Union literal of every namespaced code under `DRIVE_SHARE.*`.
### `class DriveShareError extends SuaveplanError<DriveShareErrorCode>`
- **constructor** `(code, messageKey, options?)` — same shape as `SuaveplanError`. Stamps the brand symbol on the instance.
- **static** `is(value): value is DriveShareError` — true only when `value` is `instanceof` AND carries the package-private brand symbol.
### `function registerDriveShareErrorCodes(): void`
Idempotent eager-registration hook. The codes are also registered automatically by importing this module.
## Implementation Notes
- The brand symbol uses `Symbol("@suaveplan/drive-sharing.error")` and is **not** exported, so external code cannot fabricate it.
- `DriveShareError.is()` checks both `instanceof` and the brand because `instanceof` breaks across iframe / worker realms.
- The `registered` module-private flag prevents repeat registration churn but the underlying `defineCodes` call is itself a no-op on the second invocation.
- Adding a new code is a non-breaking change; removing one or renaming one is a major-version bump per the package contract.

View File

@@ -0,0 +1,94 @@
# `otel/instruments.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the singleton OpenTelemetry instrument set for `@suaveplan/drive-sharing`. It captures a `Meter` and `Tracer` from the global OTel API on first use, builds every counter / histogram declared in the OpenSpec change, and exposes setter / reset hooks so tests can route everything through an in-memory `MeterProvider` for assertion.
## Features
- Lazy-singleton instrument acquisition: zero cost when no provider is registered (no-op meters are returned automatically by `@opentelemetry/api`).
- `setDriveShareMeterProvider` / `setDriveShareTracerProvider` accept `undefined` to fall back to the global default — handy for `afterEach` cleanup.
- Eight counters and three histograms covering partitions, grants, tokens, transfers, key rotations, policy checks, and envelope encrypt / decrypt latency.
- Safe-by-construction: only IDs (which are public-by-design), durations, and counts are recorded — never key material or plaintext bytes.
## Usage: Basic Example
```ts
import { getDriveShareInstruments } from "@suaveplan/drive-sharing/otel";
const i = getDriveShareInstruments();
i.grantCheckTotal.add(1);
i.envelopeDurationMs.record(2.7);
```
## Usage: Advanced Example
```ts
import {
getDriveShareInstruments,
setDriveShareMeterProvider,
resetDriveShareInstruments,
} from "@suaveplan/drive-sharing/otel";
const records: Array<[string, number]> = [];
const provider = {
getMeter: () => ({
createCounter: (name: string) => ({
add: (v: number) => records.push([name, v]),
}),
createHistogram: (name: string) => ({
record: (v: number) => records.push([name, v]),
}),
}),
} as unknown as Parameters<typeof setDriveShareMeterProvider>[0];
setDriveShareMeterProvider(provider);
try {
getDriveShareInstruments().grantDeniedTotal.add(1);
// assert against `records`
} finally {
setDriveShareMeterProvider(undefined);
resetDriveShareInstruments();
}
```
## API Reference
### `interface DriveShareInstruments`
The structural shape of the instrument bundle. Field summary:
| Field | Type | Notes |
|---|---|---|
| `partitionMemberTotal` | Counter | Members added |
| `grantDeniedTotal` | Counter | Grant denials with `reason` attribute |
| `grantCheckTotal` | Counter | Grant checks (allowed + denied) |
| `grantCreateTotal` | Counter | Grants created |
| `partitionCreateTotal` | Counter | Partitions created |
| `tokenCreateTotal` | Counter | Tokens created |
| `tokenValidateTotal` | Counter | Token validations |
| `tokenRevokedTotal` | Counter | Tokens revoked |
| `transferBytesTotal` | Counter | Bytes transferred |
| `keyRotateTotal` | Counter | Key rotations |
| `policyCheckDurationMs` | Histogram | Policy-check latency in ms |
| `envelopeDurationMs` | Histogram | Envelope encrypt/decrypt latency in ms |
| `transferDurationMs` | Histogram | Cross-partition transfer latency in ms |
### Functions
- `getDriveShareInstruments(): DriveShareInstruments` — returns the lazy singleton.
- `setDriveShareMeterProvider(provider | undefined): void` — overrides the meter source. `undefined` clears the override.
- `resetDriveShareInstruments(): void` — forces re-acquisition on next call.
- `getDriveShareTracer(): Tracer` — lazy singleton tracer.
- `setDriveShareTracerProvider(provider | undefined): void` — overrides the tracer source.
- `resetDriveShareTracer(): void` — forces re-acquisition on next call.
## Implementation Notes
- The module captures the OTel global state once and caches it for the rest of the process lifetime to avoid `getMeter` overhead on hot paths (grant checks, envelope encrypt) — at scale these are sub-millisecond operations and accruing 1µs of OTel overhead per call would dominate the budget.
- `setDriveShareMeterProvider(undefined)` resets to the default global provider but does **not** automatically rebuild the instrument cache — callers must invoke `resetDriveShareInstruments()` explicitly. This keeps the no-op fast path predictable.
- Counter and histogram unit strings follow the [UCUM standard](https://ucum.org/) — `ms` for durations, `By` for bytes, `{members}` etc. for dimensionless counts.
- The `description` text for each instrument is the contract that downstream observability dashboards will display; reword cautiously.

View File

@@ -0,0 +1,120 @@
# `partitions/partition-manager/partition-manager.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PartitionManager` orchestrates the lifecycle of partition records. It accepts a `PartitionStore` for persistence and a `KeyRing` for per-user key material, validates every mutation against the active policy via `validateAction`, and emits `partition:*` and `member:*` events on a caller-supplied `EventBusLike`. The manager is the single throat through which all partition state changes pass; downstream modules (grants, tokens, transfer) treat the partition as immutable input.
## Features
- Five mutating operations (`create`, `delete`, `addMember`, `removeMember`, `update`) plus three read-only operations (`get`, `list`, `getKeyRing`).
- Zod-validated inputs at every public entrypoint via `CreatePartitionOptionsSchema` / `AddMemberOptionsSchema` / `UpdatePartitionOptionsSchema`.
- Injectable `Clock` for deterministic timestamping and an injectable id generator for deterministic IDs in tests.
- `EventBusLike<PartitionManagerEvents>` carries `partition:created`, `partition:deleted`, `partition:updated`, `member:added`, `member:removed`. No key material in event payloads.
- OTel spans on every mutating operation; counter `share.partition.member_total` per partition; counter `share.partition.create_total`.
- Graceful-degradation read-only toggle (`setReadOnly`) — mutations throw `SHARE_POLICY_VIOLATION` while the flag is true.
## Usage: Basic Example
```ts
import { PartitionManager } from "@suaveplan/drive-sharing/partitions/partition-manager";
import { InMemoryPartitionStore } from "@suaveplan/drive-sharing/partitions/partition-store";
import { KeyRing } from "@suaveplan/drive-sharing/encryption/key-ring";
import { userId } from "@suaveplan/drive-sharing/types/common";
const manager = new PartitionManager({
store: new InMemoryPartitionStore(),
keyRing: new KeyRing(),
});
const partition = await manager.create({
name: "research",
type: "shared",
ownerId: userId("alice"),
});
```
## Usage: Advanced Example
```ts
import { PartitionManager } from "@suaveplan/drive-sharing/partitions/partition-manager";
import { InMemoryPartitionStore } from "@suaveplan/drive-sharing/partitions/partition-store";
import { KeyRing } from "@suaveplan/drive-sharing/encryption/key-ring";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
let now = 0;
let counter = 0;
const events = new Map<string, unknown[]>();
const bus = {
on: () => () => {},
emit: (k: string, p: unknown) => {
const arr = events.get(k) ?? [];
arr.push(p);
events.set(k, arr);
},
};
const manager = new PartitionManager({
store: new InMemoryPartitionStore(),
keyRing: new KeyRing(),
clock: () => (now += 1000),
idGenerator: () => `id-${(counter += 1)}`,
events: bus,
});
const p = await manager.create({
name: "vault",
type: "encrypted",
ownerId: userId("alice"),
policy: { maxMembers: 3 },
});
await manager.addMember(p.id, userId("bob"), { permissions: ["read"] });
manager.setReadOnly(true);
// further mutations throw SHARE_POLICY_VIOLATION
```
## API Reference
### `class PartitionManager`
- `constructor(config: PartitionManagerConfig)`
- `create(options): Promise<Partition>`
- `get(id): Promise<Partition>` — throws `SHARE_PARTITION_NOT_FOUND`.
- `list(): Promise<Partition[]>`
- `delete(id): Promise<void>`
- `addMember(id, userId, options?): Promise<Partition>` — throws `SHARE_POLICY_VIOLATION` when policy denies.
- `removeMember(id, userId): Promise<Partition>` — idempotent.
- `update(id, options): Promise<Partition>`
- `setReadOnly(value): void` / `isReadOnly(): boolean`
- `getKeyRing(): KeyRing`
### `interface CreatePartitionOptions`
`{ name, type, ownerId, policy? }`.
### `interface AddMemberOptions`
`{ permissions?, wrappedKey? }`. Default permissions are `["read"]`.
### `interface UpdatePartitionOptions`
`{ name?, policy? }`.
### `interface PartitionManagerEvents`
`partition:created` / `partition:deleted` / `partition:updated` / `member:added` / `member:removed`. No key material.
### `interface PartitionManagerConfig`
`{ store, keyRing, policy?, clock?, events?, idGenerator? }`.
## Implementation Notes
- The default policy is computed once in the constructor (`createPolicy(config.policy ?? {})`). Per-partition policies are layered on top via `mergePolicies` in `create` and `update`.
- `addMember` validates the partition's policy against the action `share` (the canonical "I am modifying membership" verb); when the policy denies, the call throws `SHARE_POLICY_VIOLATION`. Importantly, the check is performed against the partition's CURRENT membership — adding the Nth member when `maxMembers` is N triggers the denial branch.
- `removeMember` is intentionally idempotent. Calling it twice with the same userId returns the same partition the second time and does NOT emit `member:removed`. This matches the spec scenario "RemoveMember restores count".
- `setReadOnly(true)` is the spec's documented graceful-degradation hook; the partition store's health check is the trigger but the toggle is exposed publicly so consumers can wire any policy they like.
- Event emission is fire-and-forget — the `EventBusLike.emit` contract is synchronous and does not surface listener errors. Asynchronous side effects belong to the listener, not the manager.

View File

@@ -0,0 +1,108 @@
# `partitions/partition-policy/partition-policy.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The partition-policy module is the deterministic state-machine that decides whether a given action is permitted on a given partition. It exposes pure functions (`createPolicy`, `mergePolicies`, `validateAction`, `requiresEncryption`, `requiresReauth`), four canonical presets (`public`, `private`, `shared`, `encrypted`), and the default policy. Keeping this logic pure — no IO, no side effects beyond an OTel histogram — means consumers can call `validateAction` 100k+ times per second from a hot path.
## Features
- Frozen `DEFAULT_PRESETS` covering the four documented postures from the OpenSpec change.
- `mergePolicies` is commutative on disjoint array fields (`reauthRequired`, `allowedActions`) and dedupes overlapping entries.
- `validateAction` returns `{ allowed: true }` or `{ allowed: false, reason }` — never throws.
- `share.policy.check_duration_ms` histogram recorded on every `validateAction` call.
- `requiresEncryption` and `requiresReauth` are pure boolean predicates.
## Usage: Basic Example
```ts
import {
createPolicy,
validateAction,
} from "@suaveplan/drive-sharing/partitions/partition-policy";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
const policy = createPolicy({ encryption: true });
const result = validateAction(policy, {
action: "read",
userId: userId("alice"),
partition: {
id: partitionId("p"),
name: "demo",
type: "shared",
ownerId: userId("alice"),
members: [],
policy,
createdAt: 0,
updatedAt: 0,
},
});
// { allowed: true }
```
## Usage: Advanced Example
```ts
import {
DEFAULT_PRESETS,
mergePolicies,
requiresEncryption,
requiresReauth,
} from "@suaveplan/drive-sharing/partitions/partition-policy";
// Compose a stricter encrypted preset.
const strictEncrypted = mergePolicies(DEFAULT_PRESETS.encrypted, {
maxMembers: 5,
ttl: 86_400_000,
});
if (requiresEncryption(strictEncrypted)) {
console.log("AEAD required");
}
if (requiresReauth(strictEncrypted, "write")) {
console.log("re-auth before write");
}
```
## API Reference
### `function getDefaultPolicy(): PartitionPolicyConfig`
Returns `{ encryption: true, reauthRequired: [], allowedActions: ["read", "write", "delete", "share"] }`.
### `function createPolicy(config: Partial<PartitionPolicyConfig>): PartitionPolicyConfig`
Equivalent to `mergePolicies(getDefaultPolicy(), config)`.
### `function mergePolicies(base, override): PartitionPolicyConfig`
Deep-merges array fields with deduplication. Scalar fields (`encryption`, `maxMembers`, `ttl`) take the override when present.
### `function validateAction(policy, ctx): PolicyValidationResult`
- Returns `{ allowed: false, reason: "..." }` when the action is not in `allowedActions`.
- Returns `{ allowed: false, reason: "maxMembers reached..." }` when the partition is full and `userId` is not already a member.
- Returns `{ allowed: true }` otherwise.
- Records `share.policy.check_duration_ms`.
### `function requiresEncryption(policy): boolean`
Equivalent to `policy.encryption`.
### `function requiresReauth(policy, action): boolean`
Equivalent to `policy.reauthRequired.includes(action)`.
### `const DEFAULT_PRESETS`
Frozen `Record<"public" | "private" | "shared" | "encrypted", PartitionPolicyConfig>`.
## Implementation Notes
- `validateAction` checks `members.some(m => m.userId === ctx.userId)` before applying the `maxMembers` ceiling, so existing members can perform actions even when the partition is full. New members are blocked.
- The `dedup` helper uses `Set` for O(N) deduplication; arrays are tiny (max five action verbs) so this is well below any cache line.
- Each preset is wrapped in `Object.freeze` at module load so consumers cannot mutate the shared singleton.
- `validateAction` deliberately does not consult `policy.reauthRequired` — that is a separate concern (`requiresReauth`) so consumers can decide whether to surface a re-auth prompt before or after the policy check.
- Adding a new `PolicyAction` requires updating `validateAction`, the `policyActionSchema` enum in `types/schemas.ts`, and the union type in `types/common.ts`. Forgetting any of the three triggers a TypeScript error or a Zod validation error at the boundary, never silent acceptance.

View File

@@ -0,0 +1,85 @@
# `partitions/partition-store/partition-store.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `PartitionStore` interface is the persistence boundary between `PartitionManager` and the durable storage backend. Decoupling the manager from a concrete store means consumers can plug in SurrealDB, Postgres, sqlite-via-WASM, or any other adapter without forking the package. The bundled `InMemoryPartitionStore` backs tests and single-node deployments where durability across process restarts is not required.
## Features
- Four-method interface: `get`, `set`, `delete`, `list`. No transactional or batched operations — those belong to the underlying database adapter.
- Async signatures even on the in-memory implementation so the consumer code path is shape-compatible regardless of backend.
- `Map<PartitionId, Partition>` backing on the in-memory store — O(1) get / set / delete, O(N) list with reflective insertion order.
## Usage: Basic Example
```ts
import {
InMemoryPartitionStore,
type PartitionStore,
} from "@suaveplan/drive-sharing/partitions/partition-store";
const store: PartitionStore = new InMemoryPartitionStore();
await store.set({
id: "p-001" as never,
name: "demo",
type: "shared",
ownerId: "alice" as never,
members: [],
policy: { encryption: true, reauthRequired: [], allowedActions: ["read"] },
createdAt: 0,
updatedAt: 0,
});
const fetched = await store.get("p-001" as never);
```
## Usage: Advanced Example
```ts
import type { PartitionStore } from "@suaveplan/drive-sharing/partitions/partition-store";
class SurrealPartitionStore implements PartitionStore {
constructor(private readonly db: { query: (q: string) => Promise<unknown> }) {}
async get(id: PartitionId): Promise<Partition | undefined> {
const rows = (await this.db.query(`SELECT * FROM partition:${id}`)) as Partition[];
return rows[0];
}
async set(partition: Partition): Promise<void> {
await this.db.query(`UPSERT partition:${partition.id} CONTENT ${JSON.stringify(partition)}`);
}
async delete(id: PartitionId): Promise<void> {
await this.db.query(`DELETE partition:${id}`);
}
async list(): Promise<Partition[]> {
return (await this.db.query(`SELECT * FROM partition`)) as Partition[];
}
}
```
## API Reference
### `interface PartitionStore`
```ts
get(id: PartitionId): Promise<Partition | undefined>
set(partition: Partition): Promise<void>
delete(id: PartitionId): Promise<void>
list(): Promise<Partition[]>
```
### `class InMemoryPartitionStore implements PartitionStore`
Map-backed implementation. Construct with no arguments. State lives only for the lifetime of the instance.
## Implementation Notes
- `delete` is a no-op when the id is absent; this matches the spec scenario "Partition store delete is idempotent".
- `list` returns a fresh array on every call so the caller cannot mutate the underlying map by mutating the returned slice.
- Real backends should implement `set` with `INSERT … ON CONFLICT UPDATE` (or the database-specific equivalent) so concurrent writers do not lose updates. The in-memory implementation is single-threaded by construction.
- The interface deliberately omits transactional support. `PartitionManager` orchestrates multi-step state changes by reading, mutating, and writing — atomicity is the store's responsibility under contention. For the in-memory case there is no concurrency.

View File

@@ -0,0 +1,110 @@
# `sharing/grants/grants.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`GrantManager` is the deterministic in-memory grant store. It holds `Grant` records keyed by `GrantId` and answers permission queries via `check(grantId, userId, action)`. The check is exhaustive: existence, non-revocation, non-expiry, grantee identity, and permission inclusion are all verified, in that order, with the first failure short-circuiting the result. Every check increments `share.grant.check_total`; denials additionally increment `share.grant.denied_total` with a `reason` attribute so dashboards can split denials by cause.
## Features
- Synchronous `check` — sub-millisecond by design (no IO, no crypto).
- Injectable `Clock` so expiry tests are deterministic.
- `EventBusLike` integration: `grant:created` and `grant:revoked` events carry full context but never key material.
- Hard cap on active grants (`GRANT_MANAGER_MAX_ENTRIES = 10_000_000`) per the spec scalability table.
- Filtered `list(query)` supporting `granteeId`, `partitionId`, and `active` filters.
- `revoke` is idempotent — already-revoked / missing grants are no-ops.
## Usage: Basic Example
```ts
import { GrantManager } from "@suaveplan/drive-sharing/sharing/grants";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
const grants = new GrantManager();
const grant = grants.create({
grantorId: userId("alice"),
granteeId: userId("bob"),
partitionId: partitionId("p"),
permissions: ["read"],
});
grants.check(grant.id, userId("bob"), "read"); // { allowed: true }
```
## Usage: Advanced Example
```ts
import { GrantManager } from "@suaveplan/drive-sharing/sharing/grants";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
let now = 0;
const events = new Map<string, unknown[]>();
const grants = new GrantManager({
clock: () => now,
events: {
on: () => () => {},
emit: (k, p) => {
const arr = events.get(k) ?? [];
arr.push(p);
events.set(k, arr);
},
},
});
const g = grants.create({
grantorId: userId("alice"),
granteeId: userId("bob"),
partitionId: partitionId("p"),
permissions: ["read"],
expiresAt: 1000,
});
now = 999;
console.log(grants.check(g.id, userId("bob"), "read")); // allowed
now = 1001;
console.log(grants.check(g.id, userId("bob"), "read")); // denied: expired
grants.revoke(g.id);
console.log(grants.list({ active: true })); // []
```
## API Reference
### `class GrantManager`
- `constructor(config?: GrantManagerConfig)`
- `create(options: CreateGrantOptions): Grant`
- `check(id, userId, action): GrantCheckResult`
- `revoke(id): void`
- `list(query?): Grant[]`
- `get(id): Grant | undefined`
### `interface CreateGrantOptions`
`{ grantorId, granteeId, partitionId, permissions, expiresAt? }`.
### `interface Grant`
`{ id, grantorId, granteeId, partitionId, permissions, createdAt, expiresAt?, revoked }`.
### `interface GrantCheckResult`
`{ allowed: boolean; reason?: string }` — `reason` is one of `grant_not_found`, `revoked`, `expired`, `grantee_mismatch`, `permission_missing`.
### `interface GrantQuery`
`{ granteeId?, partitionId?, active? }`.
### `interface GrantManagerEvents`
- `grant:created` — full grant
- `grant:revoked` — `{ id, partitionId, userId }`
## Implementation Notes
- The denial-reason strings are stable contract — dashboards may key on them. Renaming a reason is a major-version bump.
- `revoke` is intentionally idempotent so a duplicate "revoke this grant" message from a retry-loop does not trigger a second `grant:revoked` event.
- `list({ active: true })` reads the clock once per grant; for very large grant stores callers should prefer `list({ partitionId, active: true })` so the filter narrows before the active-check fires.
- The Zod schema (`CreateGrantOptionsSchema`) enforces non-empty `permissions`. An attacker who calls `create` with `permissions: []` is rejected at the boundary, never silently inserted.
- `check` runs the action verb through `permissions.includes(action as SharePermission)` — the cast is safe because every `PolicyAction` is also a valid `SharePermission` in the canonical list.

View File

@@ -0,0 +1,105 @@
# `sharing/tokens/tokens.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TokenManager` issues, validates, revokes, and lists revocable share tokens. Every token carries an HMAC-SHA-256 integrity tag computed under a per-instance master key over the canonical wire form (id || partitionId || createdBy || sorted permissions || createdAt || expiresAt). Validation re-derives the tag in constant time and rejects any tampering. Tokens have a configurable default TTL (24 h) overridable per call via `expiresIn`.
## Features
- Constant-time integrity verification via `constantTimeEqual` from `@suaveplan/crypto-utils`.
- Injectable `Clock` for deterministic expiry tests.
- Configurable `defaultTTL` per instance (`DEFAULT_TOKEN_TTL_MS = 24 h`) and per-call `expiresIn` override.
- Hard cap on active tokens (`TOKEN_MANAGER_MAX_ENTRIES = 10_000_000`).
- `EventBusLike` integration: `token:created` / `token:revoked` carry context but never the integrity tag itself.
- OTel: `share.token.create_total`, `share.token.validate_total`, `share.token.revoked_total`, plus `share.token.create` / `validate` spans.
## Usage: Basic Example
```ts
import { TokenManager } from "@suaveplan/drive-sharing/sharing/tokens";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
const tokens = new TokenManager();
const token = tokens.create({
partitionId: partitionId("p"),
createdBy: userId("alice"),
permissions: ["read"],
});
const result = tokens.validate(token.id);
console.log(result.valid); // true
```
## Usage: Advanced Example
```ts
import { TokenManager } from "@suaveplan/drive-sharing/sharing/tokens";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
let now = 0;
const events: Array<[string, unknown]> = [];
const masterKey = new Uint8Array(32).fill(0x42);
const tokens = new TokenManager({
clock: () => now,
defaultTTL: 60_000,
masterKey,
events: { on: () => () => {}, emit: (k, p) => events.push([k, p]) },
});
const token = tokens.create({
partitionId: partitionId("p"),
createdBy: userId("alice"),
permissions: ["read"],
expiresIn: 5_000,
});
now = 1_000;
console.log(tokens.validate(token.id).valid); // true
now = 6_001;
console.log(tokens.validate(token.id)); // { valid: false, reason: "expired" }
tokens.revoke(token.id);
console.log(tokens.validate(token.id)); // { valid: false, reason: "revoked" }
```
## API Reference
### `class TokenManager`
- `constructor(config?: TokenManagerConfig)`
- `create(options: CreateTokenOptions): ShareToken`
- `validate(id: TokenId): TokenValidationResult`
- `revoke(id: TokenId): void`
- `list(): ShareToken[]`
### `interface CreateTokenOptions`
`{ partitionId, createdBy, permissions, expiresIn? }`. `permissions` must be non-empty (Zod-enforced).
### `interface ShareToken`
`{ id, partitionId, createdBy, permissions, createdAt, expiresAt, revoked, integrity }`.
### `interface TokenValidationResult`
`{ valid, token?, reason? }` — `reason` is one of `not_found`, `revoked`, `expired`.
### `interface TokenManagerConfig`
`{ clock?, defaultTTL?, masterKey?, events?, idGenerator? }`. Default master key is a fresh 32-byte CSPRNG buffer; default TTL is 24 hours.
### `interface TokenManagerEvents`
- `token:created` — full token (including the integrity tag, since the consumer who created the token already knows it)
- `token:revoked` — `{ id, partitionId }`
## Implementation Notes
- The canonical wire form sorts `permissions` before serialising so two tokens with the same logical content but different array order produce identical integrity tags. This protects against a subtle bug where a downstream serializer reorders array elements.
- The HMAC key (`masterKey`) is held in instance state — it never appears in events, logs, or token bodies. Rotation is done by constructing a new `TokenManager`; the old one's tokens become invalid because the new master key produces different integrity tags.
- `validate` always recomputes the integrity tag, even when the token is revoked or expired. This is deliberate: an attacker who has tampered with the in-memory token map cannot evade detection by also setting `revoked = true`. (We DO short-circuit on `not_found`, since a missing token cannot have been tampered with.)
- The Zod schema rejects `expiresIn: 0` and negative values; these would produce already-expired tokens which are dead on arrival.
- `revoke` is idempotent so a duplicate revocation does not double-count `share.token.revoked_total`.

View File

@@ -0,0 +1,110 @@
# `transfer/transfer/transfer.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TransferManager` performs cross-partition file transfers. Given a source partition, target partition, file id, and user id, it resolves both KEKs via injected callbacks, retrieves the file's wrapped DEK (or plaintext DEK for the test path), validates source-and-target policies, rewraps the DEK atomically with `rewrapKey`, persists a `TransferRecord`, and returns the new `WrappedKey`. The whole flow is two-phase (resolve → validate → rewrap → record) with rollback on failure: if any step throws, no `TransferRecord` for the success path is created.
## Features
- AbortSignal support throughout: pre-aborted signals reject immediately, in-flight aborts reject with `SHARE_TRANSFER_FAILED` via a `Promise.race` against an abort listener.
- Two source modes: `wrappedKeyResolver` for production (rewrap without exposing plaintext) and `fileKeyStore` for tests (a `Map<FileId, Uint8Array>` of plaintext DEKs).
- Policy validation via `validateAction` from `partition-policy`. Source check is `share` (export) or `read` (import); target check is always `write`.
- OTel: `share.transfer` span, `share.transfer.duration_ms` histogram, `share.transfer.bytes_total` counter.
- Records persisted in-memory; `getRecord(id)` and `listRecords()` expose the audit trail.
## Usage: Basic Example
```ts
import { TransferManager } from "@suaveplan/drive-sharing/transfer";
import {
fileId,
partitionId,
userId,
} from "@suaveplan/drive-sharing/types/common";
const transfer = new TransferManager({
kekResolver: async () => new Uint8Array(32).fill(0xaa),
policyResolver: async (id) => ({
policy: { encryption: true, reauthRequired: [], allowedActions: ["read", "write", "share"] },
partition: {
id,
name: "demo",
type: "shared",
ownerId: userId("alice"),
members: [{ userId: userId("alice"), permissions: ["read", "write", "share"], addedAt: 0 }],
policy: { encryption: true, reauthRequired: [], allowedActions: ["read", "write", "share"] },
createdAt: 0,
updatedAt: 0,
},
}),
fileKeyStore: new Map([[fileId("f-1"), new Uint8Array(32).fill(0x10)]]),
});
const result = await transfer.transfer({
fileId: fileId("f-1"),
sourcePartitionId: partitionId("p-1"),
targetPartitionId: partitionId("p-2"),
userId: userId("alice"),
direction: "export",
});
console.log(result.success); // true
```
## Usage: Advanced Example
```ts
import { TransferManager } from "@suaveplan/drive-sharing/transfer";
const ac = new AbortController();
setTimeout(() => ac.abort(), 0);
await transfer.transfer({
fileId: fileId("f-1"),
sourcePartitionId: partitionId("p-1"),
targetPartitionId: partitionId("p-2"),
userId: userId("alice"),
direction: "export",
signal: ac.signal,
}); // throws DriveShareError(SHARE_TRANSFER_FAILED)
```
## API Reference
### `class TransferManager`
- `constructor(config: TransferConfig)`
- `transfer(options: TransferOptions): Promise<TransferResult>` — throws `DriveShareError` on any failure.
- `getRecord(id): TransferRecord | undefined`
- `listRecords(): TransferRecord[]`
### `interface TransferConfig`
`{ kekResolver, policyResolver, wrappedKeyResolver?, fileKeyStore?, crypto?, clock?, algorithm? }`.
### `interface TransferOptions`
`{ fileId, sourcePartitionId, targetPartitionId, userId, direction, signal? }`.
### `type KekResolver`
`(userId, partitionId) => Promise<Uint8Array>` — must throw if no KEK exists for the user/partition pair.
### `type PolicyResolver`
`(partitionId) => Promise<{ policy, partition }>` — returns both so the validator can check `maxMembers` and other partition-level state.
### `type WrappedKeyResolver`
`(fileId, partitionId) => Promise<WrappedKey | undefined>` — returns the wrapped DEK currently stored for the file in the given partition.
## Implementation Notes
- The abort handler is attached BEFORE the rewrap kicks off and removed in `finally`, so an abort that arrives between resolve and rewrap correctly fires.
- `runTransfer` is the work function; it never reads `options.signal` directly (the outer wrapper handles abort) except for an early `signal.aborted` short-circuit between resolve and rewrap. This bounds the wasted work to two policy resolves and one wrapped-key fetch.
- Source action verb: `share` for export, `read` for import. The intuition is "exporting" requires explicit grant/share authority on the source; "importing" only requires read access on the source plus write on the target.
- Failure path also persists a `TransferRecord` with `success: false` (id prefixed `fail:`) so dashboards can split successful and failed transfers.
- The `crypto?: CryptoProvider` config field is currently a hook for future custom providers; the production rewrap path uses the package's default provider via `rewrapKey`. Substituting a HSM-backed provider will require threading the override through the helper functions; this is documented as a follow-up in the spec's "Out of scope" section.
- The `wrappedKeyResolver` path is the primary security-hardened production path: plaintext DEK never leaves the rewrap function. The `fileKeyStore` path is convenient for tests where the caller already has the plaintext DEK in hand.

View File

@@ -0,0 +1,92 @@
# `types/common.ts`
## Purpose
This module declares the foundational types shared by every other module in `@suaveplan/drive-sharing`. It owns the branded ID types, the wrapped-key wire shape, the partition / member / policy / grant / token / transfer record interfaces, and the `Result<T, E>` discriminated union helper. Centralising these declarations means downstream modules (encryption, partitions, sharing, transfer) consume one canonical type instead of drifting copies, and the public API surface is fully typed at every boundary.
## Features
- Six branded IDs (`UserId`, `PartitionId`, `GrantId`, `TokenId`, `FileId`, `KeyId`) implemented via the nominal `Brand<T, B>` helper.
- `WrappedKey` carries an algorithm tag plus a SHA-256 checksum so consumers can detect tampering without unwrapping.
- `EncryptedBundle` separates `nonce`, `ciphertext`, and (optional) `tag` so AEAD primitives that produce them as distinct outputs round-trip cleanly.
- `Result<T, E>` and the `ok` / `err` helpers provide a non-throwing return shape for predicates that prefer railway-style flow.
- `generateId` accepts an optional RNG for deterministic test runs and falls back to `crypto.randomUUID` (or RFC 4122 v4 if unavailable).
- `FileKeyStore` is a `Map<FileId, Uint8Array>` alias used by `TransferManager`.
## Usage: Basic Example
```ts
import {
type UserId,
userId,
type PartitionId,
partitionId,
ok,
err,
} from "@suaveplan/drive-sharing/types/common";
const u: UserId = userId("alice");
const p: PartitionId = partitionId("p-001");
const r = u === ("alice" as UserId) ? ok(u) : err(new Error("mismatch"));
```
## Usage: Advanced Example
```ts
import {
type Partition,
type PartitionPolicyConfig,
generateId,
partitionId,
userId,
} from "@suaveplan/drive-sharing/types/common";
// Deterministic IDs in tests via a stub RNG.
let counter = 0;
const stubRng = () => `id-${(counter += 1)}`;
const policy: PartitionPolicyConfig = {
encryption: true,
reauthRequired: ["admin"],
allowedActions: ["read", "write"],
maxMembers: 100,
};
const partition: Partition = {
id: partitionId(generateId(stubRng)),
name: "research",
type: "shared",
ownerId: userId("alice"),
members: [],
policy,
createdAt: 0,
updatedAt: 0,
};
```
## API Reference
### Branded IDs
`UserId`, `PartitionId`, `GrantId`, `TokenId`, `FileId`, `KeyId` — each `Brand<string, "...">`. Construct via the matching factory function.
### Factories
- `userId(value)`, `partitionId(value)`, `grantId(value)`, `tokenId(value)`, `fileId(value)`, `keyId(value)` — type-cast a raw `string` into the branded shape.
- `generateId(rng?: () => string): string` — returns a fresh ID. When `rng` is supplied its return value is used verbatim; otherwise `crypto.randomUUID` is preferred and an RFC 4122 v4 fallback is used in environments lacking it.
### `Result<T, E>`
Discriminated union. Helpers `ok(value)` and `err(error)` construct each arm.
### Wire types
`WrappedKey`, `EncryptedBundle`, `PartitionMember`, `PartitionPolicyConfig`, `Partition`, `PolicyValidationContext`, `PolicyValidationResult`, `TransferRecord`, `TransferResult`. Every shape is `readonly` and immutably typed.
## Implementation Notes
- All branded IDs are erased at runtime (`as UserId`); the brand exists only in the type system. This keeps the runtime cost zero while still preventing cross-domain misuse at compile time.
- `generateId` deliberately delegates to `crypto.randomUUID` when present so we benefit from the runtime CSPRNG; the manual fallback path is exercised only in legacy environments and is still implemented in terms of `crypto.getRandomValues` (never `Math.random()`).
- Every algorithm tag in `EncryptionAlgorithm` (`AES-GCM` / `XChaCha20-Poly1305` / `AES-KW`) is recognised by `envelopeEncrypt` and `wrapKey` — adding a new tag requires updating both modules.
- Policy fields use `readonly` arrays to prevent in-place mutation by downstream consumers; mutations must go through `PartitionManager.update`.

View File

@@ -0,0 +1,70 @@
# `types/schemas.ts`
## Purpose
This module declares Zod schemas for every public option type accepted by `@suaveplan/drive-sharing`. Schemas are wired at the public API boundary of every module so the package raises a descriptive `ZodError` before any mutation is performed. Schemas double as the canonical reference for valid inputs and are reused by `tests/property.test.ts` to drive fuzz cases without re-declaring shapes.
## Features
- One schema per public option / config type: `WrappedKeySchema`, `PartitionPolicyConfigSchema`, `EnvelopeEncryptOptionsSchema`, `KeyRingConfigSchema`, `CreatePartitionOptionsSchema`, `AddMemberOptionsSchema`, `UpdatePartitionOptionsSchema`, `PartitionManagerOptionsSchema`, `CreateGrantOptionsSchema`, `CreateTokenOptionsSchema`, `TokenManagerConfigSchema`, `TransferOptionsSchema`, `KeyRingExportSchema`.
- `WrappedKeySchema` enforces a 64-hex-char SHA-256 checksum so corrupt entries cannot pass without explicit handling.
- `AadSchema` rejects empty AAD which is the central security guarantee for envelope encryption.
- `KeyRingExportSchema` pins the export wire format to `keyring_format_version: 1` so old or future versions are rejected at parse time.
- `PartitionPolicyConfigSchema` accepts only the five canonical action verbs and rejects unknown strings.
## Usage: Basic Example
```ts
import {
CreatePartitionOptionsSchema,
} from "@suaveplan/drive-sharing/types/schemas";
const result = CreatePartitionOptionsSchema.safeParse({
name: "research",
type: "shared",
ownerId: "alice",
});
if (!result.success) {
throw new Error(result.error.message);
}
```
## Usage: Advanced Example
```ts
import z from "zod";
import {
PartitionPolicyConfigSchema,
KeyRingExportSchema,
} from "@suaveplan/drive-sharing/types/schemas";
// Build a strict policy parser that auto-fills defaults.
const StrictPolicy = PartitionPolicyConfigSchema.transform((p) => ({
encryption: p.encryption,
reauthRequired: p.reauthRequired,
allowedActions: p.allowedActions,
maxMembers: p.maxMembers ?? 1000,
ttl: p.ttl ?? 0,
}));
// Validate an externally-supplied keyring backup file before importing.
function ingest(json: unknown): z.infer<typeof KeyRingExportSchema> {
return KeyRingExportSchema.parse(json);
}
```
## API Reference
Every export is a Zod schema instance. The schemas accept the public option types declared in `types/common.ts` and their narrower wire-format counterparts. Notable contracts:
- `AadSchema` — rejects empty `Uint8Array`.
- `WrappedKeySchema` — `algorithm` is one of `AES-GCM`, `XChaCha20-Poly1305`, `AES-KW`; `data` is a non-empty `Uint8Array`; `checksum` matches `/^[0-9a-f]{64}$/`.
- `PartitionPolicyConfigSchema` — `allowedActions` and `reauthRequired` are arrays of the canonical five verbs; unknown verbs raise `ZodError`.
- `KeyRingExportSchema` — `keyring_format_version` is the literal `1`.
## Implementation Notes
- The schemas in this module deliberately avoid `.passthrough()` — extra fields are stripped to prevent attackers from smuggling state into the package.
- `Uint8Array` checks rely on `z.instanceof(Uint8Array)` which works under both Node and the browser because the global is the same.
- The `partial()` schemas (used by `policy?: Partial<PartitionPolicyConfig>`) preserve the `readonly` nature of the parent type at runtime: missing fields stay missing rather than being filled with `undefined`.
- Adding a new field to a schema is non-breaking when the field is optional. Tightening an existing constraint is a major-version bump.