Files
suaveplan/openspec/archive/2026-05-04-enterprise-drive-sharing/proposal.md
2026-05-27 23:29:53 +01:00

780 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenSpec Change: enterprise-drive-sharing
- **Change ID**: enterprise-drive-sharing
- **Package**: `@suaveplan/drive-sharing`
- **Category**: universal
- **Target path**: `packages/drive/drive-sharing`
- **Status**: PROPOSED
- **Date**: 2026-04-24
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/drive-sharing.md`
---
## Summary
Deliver `@suaveplan/drive-sharing` on the genesis branch from scratch at enterprise quality. The enterprise spec describes it as "Partitioned drive with envelope encryption". The package provides partitioned drive management with per-partition policies, envelope encryption (key-wrapping via WebCrypto), a key ring for multi-user key management, sharing grants (access control), revocable share tokens, cross-partition file transfer, and typed branded ID utilities. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all public option types, OTel instrumentation, `AbortSignal` cancellation, injectable clock and RNG, property-based tests (fast-check), and a mitata benchmark suite. The `@suaveplan/permissions` package is not yet implemented on genesis; the permissions surface is inlined here (policy validation) and will be extracted when `@suaveplan/permissions` lands.
---
## Gap analysis: sub-standard → enterprise
### Security gaps
| Main-branch / prior art | Genesis fix |
|---|---|
| Envelope encryption used AES-KW without AAD, permitting cut-and-paste attacks | `envelopeEncrypt` requires AAD (partition ID + file ID) and fails open on missing AAD |
| Key ring stored wrapped keys in a plain `Map` with no integrity checks | `KeyRing` validates wrapped-key algorithm tags and checksums on `getKey`; corrupted entries throw `SHARE_KEY_NOT_FOUND` |
| No key rotation strategy — re-encryption required manual file-by-file traversal | `KeyRing.rotateKey` re-wraps the user key; `TransferManager` propagates re-wrapped keys across partitions atomically |
| WebCrypto provider was hardcoded with no alternative | `CryptoProvider` interface is injectable; `WebCryptoProvider` is the default but callers may substitute FIPS-validated or HSM-backed providers |
| Token and grant payloads carried plaintext user IDs and partition IDs without signing | Share tokens include an integrity MAC computed via `computeIntegrity`; grants carry `createdAt` and `expiresAt` signed by the partition owner |
| No audit trail for grant creation or revocation | `EventBusLike` emits `grant:created`, `grant:revoked`, `key:rotated` events with full context (no key material) |
### Reliability gaps
| Main-branch / prior art | Genesis fix |
|---|---|
| Token TTL was a hardcoded constant (24 h) with no override | `TokenManagerConfig.defaultTTL` is configurable per instance; `CreateTokenOptions.expiresIn` overrides per token |
| Grant expiry had no clock injection, making time-based tests flaky | `TokenManager` and `PartitionManager` accept injectable `Clock`; all expiry checks use `clock.now()` |
| Partition policy validation was ad-hoc boolean logic | `validateAction` uses a deterministic state machine with exhaustive `PolicyAction` enumeration; Zod schemas prevent invalid policy shapes |
| Cross-partition transfer had no retry or atomicity guarantees | `TransferManager` implements two-phase transfer (export → validate → import) with rollback on failure |
| Key ring import/export had no format versioning | `KeyRing.export` produces versioned JSON (`keyring_format_version: 1`); `import` rejects unknown versions |
### Observability gaps
| Main-branch / prior art | Genesis fix |
|---|---|
| No per-partition metrics | Counter `share.partition.member_total` per partition; histogram `share.partition.policy_check_duration_ms` |
| No span correlation across envelope encryption boundaries | `envelopeEncrypt` / `envelopeDecrypt` create `SpanKind.INTERNAL` spans parented under the calling `drive.read` / `drive.write` span with `encryption.algorithm` attribute |
| No visibility into grant check denial reasons | `GrantCheckResult` carries `reason` string; denied checks emit `share.grant.denied_total` counter with `reason` attribute |
| No structured logging for key-ring events | `KeyRing` events (`key:added`, `key:removed`, `key:rotated`) are logged at `info` level with `user_id` and `partition_id` attributes |
### Testability gaps
| Main-branch / prior art | Genesis fix |
|---|---|
| No property-based tests for grant expiry | fast-check invariant: for any TTL T and clock offset C, `grantManager.check` returns `allowed: false` when `C > T` |
| No property-based tests for token revocation finality | fast-check invariant: once `tokenManager.revoke(id)` is called, `validate(id).valid` is always `false` for all subsequent calls |
| No determinism in policy merge tests | Injectable `Clock` and deterministic `mergePolicies` enable reproducible property tests for non-conflicting field commutativity |
| No load-test criteria for 10K+ grant checks | mitata benchmark `grant_check_10k` defines a regression gate (< 50 ms total) |
### Operational gaps
| Main-branch / prior art | Genesis fix |
|---|---|
| No preset tuning for partition policies | `DEFAULT_PRESETS` (`public`, `private`, `shared`, `encrypted`) provide ready-made policies with documented security postures |
| No memory pressure handling during bulk key ring operations | `KeyRing` operations stream over entries; `export` yields an array but caps at 10 000 entries with pagination metadata |
| No graceful degradation on partition store failure | `PartitionManager` falls back to read-only mode when `PartitionStore` is unreachable; mutations return `SHARE_POLICY_VIOLATION` with `retryable: true` |
| No circuit breaker for crypto provider | `CryptoProvider` operations time out after 5 s; repeated timeouts trigger read-only mode for encryption-dependent partitions |
---
## Security model
### Threat model
1. **Insider threat (partition member escalation)** — A member with `read` permission must not be able to elevate to `write` or `admin` without the partition owner's explicit grant.
2. **External threat (token theft or replay)** — A stolen share token must be unusable after revocation or expiry; tokens must not be forgeable without the partition master key.
3. **Compromised key-ring storage** — If the `PartitionStore` or `KeyRing` backing store is compromised, the attacker gains only wrapped keys; plaintext data keys remain inside the `Drive` process boundary.
### Encryption boundaries
- **Plaintext lives only in** the `Drive` process memory during `envelopeEncrypt` / `envelopeDecrypt` and inside the `TransferManager` re-encryption buffer.
- **Plaintext never lives in** `PartitionStore`, `KeyRing` exports, grant metadata, token storage, or logs.
- Wrapped keys (encrypted under the master key) are the only key material that crosses the process boundary into storage.
- Master key material is held only by `KeyRing` and is never serialised or logged.
### Key derivation hardening
- **Envelope encryption keys** are generated by the injectable `CryptoProvider` (default: `crypto.subtle.generateKey`) with 256-bit entropy.
- **Key-wrapping keys** (KEKs) are derived from the partition master key via HKDF-SHA-256 with a context string that includes the partition ID and user ID, preventing cross-partition and cross-user key unwrapping.
- **Master key rotation**: `KeyRing.rotateKey` generates a new master key, re-wraps all user keys, and emits a `key:rotated` event. Old master keys are zeroed from memory immediately after rotation.
### Authenticated encryption
- **AEAD modes**: AES-256-GCM (default) or XChaCha20-Poly1305 (configurable via `EncryptionAlgorithm`).
- **Nonce management**: 96-bit random nonces for GCM, 192-bit for XChaCha20-Poly1305, generated per `envelopeEncrypt` call via the injectable `Rng`.
- **AAD**: Every envelope encryption includes `AAD = partitionId \|\| fileId` to bind the ciphertext to a specific partition and file, preventing cut-and-paste across contexts.
- **Tag verification**: AEAD tag verification is constant-time; decryption failures throw `SHARE_ENCRYPTION_FAILED` without revealing whether the ciphertext or the AAD was invalid.
### Side-channel resistance
- **Constant-time key comparison** — `KeyRing.getKey` comparisons and `CryptoProvider` unwrap validation use timing-safe equality.
- **Timing-safe integrity verification** — `verifyIntegrity` uses constant-time comparison; no early return on mismatch.
- **Memory clearing** — Unwrapped data keys and master key buffers are overwritten with zeros after use (`Uint8Array.fill(0)`) where the runtime permits.
### Key rotation strategy
- **User-level rotation**: `KeyRing.rotateKey(userId, newKey)` re-wraps the user's key under a new master key. The old wrapped key is replaced atomically.
- **Partition-level rotation**: When a partition master key is rotated, all member keys are re-wrapped in a single transaction. `TransferManager` uses the new KEK for all subsequent cross-partition moves.
- **Token invalidation on rotation**: All active share tokens for a partition are revoked automatically when the partition master key is rotated, forcing re-authentication.
---
## Performance SLOs
### Latency budgets
| Operation | p50 | p99 | Notes |
|---|---|---|---|
| `grantManager.check` | < 0.5 ms | < 2 ms | In-memory lookup, no IO |
| `tokenManager.validate` | < 0.5 ms | < 2 ms | In-memory lookup + HMAC verify |
| `keyRing.getKey` | < 1 ms | < 5 ms | Unwrap via WebCrypto |
| `envelopeEncrypt` (1 MiB) | < 10 ms | < 50 ms | AES-256-GCM via WebCrypto |
| `envelopeDecrypt` (1 MiB) | < 10 ms | < 50 ms | AES-256-GCM via WebCrypto |
| `transferManager.transfer` (1 MiB) | < 50 ms | < 200 ms | Export + re-encrypt + import |
| `partitionManager.create` | < 5 ms | < 20 ms | Policy validation + store write |
### Throughput guarantees
- **Grant checks**: ≥ 100 000 checks/sec single-threaded (in-memory, no IO).
- **Token validations**: ≥ 100 000 validations/sec single-threaded.
- **Envelope encryption throughput**: ≥ 100 MiB/sec for 1 MiB–100 MiB files via WebCrypto (hardware-accelerated AES-GCM).
- **Cross-partition transfer**: ≥ 50 MiB/sec sustained for files 10 MiB–1 GiB, including re-encryption overhead.
### Memory bounds
- **Max resident memory during 1 GiB cross-partition transfer**: ≤ 32 MiB (streaming 4 MiB blocks with bounded re-encryption buffers).
- **Key ring**: ≤ 1 KiB per entry (user ID + wrapped key metadata).
- **Grant store**: ≤ 512 bytes per grant.
- **Token store**: ≤ 256 bytes per token.
### Chunk size optimization and dedup savings targets
- *Not applicable to `@suaveplan/drive-sharing`* — chunking and dedup are handled by the underlying `@suaveplan/drive` package. Sharing operates on whole-file keys and metadata.
### Scalability limits
| Resource | Soft limit | Hard limit |
|---|---|---|
| Max partitions | 100 000 | 1 000 000 |
| Max members per partition | 1 000 | 10 000 |
| Max grants active | 1 000 000 | 10 000 000 |
| Max tokens active | 1 000 000 | 10 000 000 |
| Max keys in key ring | 10 000 | 100 000 |
| Max concurrent transfers | 64 | 256 |
---
## Observability plan
### OTel spans
| Public operation | Span name | SpanKind | Attributes |
|---|---|---|---|
| `partitionManager.create` | `share.partition.create` | `SERVER` | `partition.id`, `partition.type`, `partition.owner_id` |
| `partitionManager.addMember` | `share.partition.add_member` | `SERVER` | `partition.id`, `user.id` |
| `grantManager.create` | `share.grant.create` | `SERVER` | `grant.id`, `grantor.id`, `grantee.id`, `partition.id` |
| `grantManager.check` | `share.grant.check` | `INTERNAL` | `grant.id`, `user.id`, `action`, `result` |
| `tokenManager.create` | `share.token.create` | `SERVER` | `token.id`, `partition.id`, `created_by` |
| `tokenManager.validate` | `share.token.validate` | `INTERNAL` | `token.id`, `valid`, `reason` |
| `transferManager.transfer` | `share.transfer` | `SERVER` | `file.id`, `source_partition`, `target_partition`, `direction` |
| `keyRing.addKey` | `share.key.add` | `INTERNAL` | `user.id`, `key.algorithm` |
| `keyRing.rotateKey` | `share.key.rotate` | `INTERNAL` | `user.id`, `partition.id` |
| `envelopeEncrypt` | `share.envelope.encrypt` | `INTERNAL` | `encryption.algorithm`, `aad.length` |
| `envelopeDecrypt` | `share.envelope.decrypt` | `INTERNAL` | `encryption.algorithm`, `aad.length` |
### Metrics
| Metric name | Type | Unit | Description |
|---|---|---|---|
| `share.partition.create_total` | Counter | 1 | Partitions created |
| `share.partition.member_total` | Counter | 1 | Members added per partition |
| `share.grant.create_total` | Counter | 1 | Grants created |
| `share.grant.check_total` | Counter | 1 | Grant checks performed |
| `share.grant.denied_total` | Counter | 1 | Denied grant checks |
| `share.token.create_total` | Counter | 1 | Tokens created |
| `share.token.validate_total` | Counter | 1 | Token validations performed |
| `share.token.revoked_total` | Counter | 1 | Tokens revoked |
| `share.transfer.duration_ms` | Histogram | ms | Cross-partition transfer latency |
| `share.transfer.bytes_total` | Counter | bytes | Bytes transferred |
| `share.key.rotate_total` | Counter | 1 | Key rotations performed |
| `share.envelope.duration_ms` | Histogram | ms | Envelope encrypt/decrypt latency |
| `share.policy.check_duration_ms` | Histogram | ms | Policy validation latency |
### Structured logging
- All log lines are JSON with `timestamp`, `level`, `msg`, `trace_id`, `span_id`, `service.name` (`@suaveplan/drive-sharing`).
- Sensitive fields (master keys, unwrapped data keys, wrapping keys) are redacted with `[REDACTED]`.
- Lifecycle events (`partition:created`, `key:rotated`, `grant:revoked`) emitted via `EventBusLike` are also logged at `info` level.
### Health checks
- `PartitionStore.health(): Promise<HealthStatus>` — ping the backing store every 30 s.
- `KeyRing.health(): HealthStatus` — validates that the master key is present and at least one crypto operation succeeds.
- `DriveShare.health(): HealthStatus` — aggregates partition store health, key-ring health, and crypto provider readiness.
### Alerting thresholds
| Alert | Condition | Severity |
|---|---|---|
| Grant denial spike | `share.grant.denied_total > 100 / min` | warning |
| Token validation failure rate | `share.token.validate_total` with `valid: false` > 5 % | warning |
| Transfer latency p99 | `share.transfer.duration_ms` p99 > 500 ms | warning |
| Key rotation failure | `SHARE_KEY_NOT_FOUND` or `SHARE_ENCRYPTION_FAILED` during rotation | critical |
| Partition store unavailable | `share.partition.create_total` drops to zero with store errors | critical |
| Crypto provider timeout | `share.envelope.duration_ms` p99 > 5 s | critical |
---
## Dependency audit
### Runtime dependencies
| Package | Version constraint | Justification | Supply-chain risk | Tree-shakeable | License |
|---|---|---|---|---|---|
| `@suaveplan/drive` | `workspace:^` | Underlying virtual filesystem for partition storage | Low (monorepo, same CI) | Yes | Proprietary |
| `@suaveplan/error` | `workspace:^` | Structured error base class and code registry | Low (monorepo) | Yes | Proprietary |
| `@suaveplan/types` | `workspace:^` | Injectable `Clock`, `Rng`, `LoggerLike`, `EventBusLike` | Low (monorepo) | Yes | Proprietary |
| `@suaveplan/crypto` | `workspace:^` | AEAD, sign, agree — envelope encryption primitives | Low (monorepo) | Yes | Proprietary |
| `@suaveplan/crypto-keys` | `workspace:^` | `KeyPair`, `Keychain` — key ring key material management | Low (monorepo) | Yes | Proprietary |
| `@suaveplan/events` | `workspace:^` | Event bus for key ring and partition lifecycle | Low (monorepo) | Yes | Proprietary |
| `zod` | `catalog:forms` | Runtime validation of all public option types | Medium (npm, widely audited) | Yes | MIT |
| `@opentelemetry/api` | `catalog:observability` | OTel span and metric APIs only | Low (CNCF, stable spec) | Yes | Apache-2.0 |
| `tslib` | `catalog:build` | TypeScript runtime helpers | Low (Microsoft) | Yes | 0BSD |
### Supply-chain risk assessment
- **Workspace packages** (`@suaveplan/*`) are built from the same monorepo lockfile (`bun.lock`) and published to a private Verdaccio instance. Compromise requires monorepo CI access.
- **Zod** is a mature, widely audited library with no native dependencies. Pinned via `catalog:forms` and verified against `bun.lock`.
- **@opentelemetry/api** is a stable API surface (v1.x) maintained by the CNCF. No telemetry data leaves the process unless the host application wires an exporter.
- **tslib** is a Microsoft-maintained zero-dependency runtime helper. License (0BSD) is maximally permissive.
### Tree-shakeability impact
- All `@suaveplan/*` workspace packages are authored as ES modules with explicit exports and no side-effectful top-level code.
- `zod` tree-shakes well; only used schemas are retained.
- `@opentelemetry/api` is API-only; the SDK and exporters are not bundled.
- **Expected dead-code elimination**: if a consumer uses only grant/token management without cross-partition transfer, the resulting bundle should exclude `TransferManager` and `envelopeEncrypt` implementations.
### License compatibility
- All runtime dependencies are permissively licensed (MIT, Apache-2.0, 0BSD, or proprietary internal).
- No GPL, LGPL, or AGPL dependencies at runtime.
- Proprietary `@suaveplan/*` packages are governed by the Suaveplan commercial license.
---
## Operational readiness
### Graceful degradation
- **Read-only mode**: If `PartitionStore` reports unhealthy, `PartitionManager` transitions to read-only. `create`, `addMember`, `removeMember`, and `update` return `SHARE_POLICY_VIOLATION` with `retryable: true` and `degraded: true`. `get`, `list`, `grantManager.check`, and `tokenManager.validate` continue to operate.
- **Encryption bypass disabled**: If the crypto provider is unavailable, encrypted partitions refuse all operations rather than fall back to plaintext. Public partitions (encryption disabled) remain usable.
### Resource limits
| Resource | Default limit | Configurable via |
|---|---|---|
| Max concurrent transfers | 64 | `TransferConfig.maxConcurrent` |
| Max snapshot retention (per partition) | 10 000 | `PartitionManager` constructor option |
| Max tokens per partition | 10 000 | `TokenManager` internal cap (LRU eviction) |
| Max grants per partition | 10 000 | `GrantManager` internal cap |
| Max keys per key ring | 100 000 | `KeyRing` internal cap |
| Max policy check cache size | 1 000 | `PartitionManager` LRU |
### Error recovery
- **Transient partition store unavailability**: Exponential backoff with jitter (initial 50 ms, max 2 s, 3 retries). After retries, transition to read-only mode.
- **Transfer failure mid-operation**: `TransferManager` implements two-phase transfer. If the import phase fails after a successful export, the exported file key is discarded and the transfer record is marked `success: false`.
- **Crypto provider timeout**: Operations time out after 5 s. Repeated timeouts (3 in 60 s) trigger read-only mode for encryption-dependent partitions.
### Circuit breaker for storage backend
- **Failure threshold**: 5 errors in 60 seconds.
- **Recovery timeout**: 30 seconds.
- **Half-open probe**: 1 `PartitionStore.get` request every 10 seconds during recovery.
- **Metric emitted**: `share.storage_circuit_state` (gauge: 0=closed, 1=open, 2=half-open).
### Backup/restore compatibility
- **Backup**: `KeyRing.export` produces a portable JSON array of `UserWrappedKey` objects. The format is versioned (`keyring_format_version: 1`) and documented.
- **Restore**: `KeyRing.import` validates the format version and algorithm tags before ingesting. Unknown format versions throw `SHARE_KEY_NOT_FOUND` with `unsupported_format: true`.
- **Partition backup**: `PartitionStore.list` + individual `get` calls can be serialised to a snapshot archive compatible with `@suaveplan/drive` `SnapshotManager`.
- **Cross-preset compatibility**: Key rings exported from the `encrypted` preset can be imported into the `shared` preset provided the same master key is supplied.
---
## Out of scope
The following capabilities are explicitly out of scope for this change:
- **User management** — identity providers, authentication flows, and user directories are handled by `@suaveplan/auth`.
- **Billing integration** — no metered usage, payment gateways, or quota enforcement.
- **Real-time collaboration (shared cursors)** — no WebSocket presence, operational transforms, or live editing.
- **Audit log storage** — event emission is wired, but persistent audit log retention is delegated to downstream consumers.
- **Public link indexing** — no search engine optimisation, crawlers, or public directory listings.
---
## Verification approach
### Coverage target
- **100/100/100/100** coverage (statements/branches/functions/lines) required before merge.
- Istanbul coverage reports generated by `@vitest/coverage-istanbul` and gated in CI.
### Property-based test invariants (fast-check)
| Invariant | Generator | Assertion |
|---|---|---|
| Grant expiry | `fc.integer({ min: 1, max: 86400 })` TTL, `fc.integer()` clock offsets | `check(grantId)` returns `allowed: false` when `clock.now() > createdAt + TTL` |
| Token revocation finality | `fc.string()` token IDs | After `revoke(id)`, `validate(id).valid` is always `false` |
| Policy merge commutativity | `fc.record({ encryption: fc.boolean(), maxMembers: fc.integer() })` | `mergePolicies(a, b)` for non-overlapping fields equals `{ ...a, ...b }` |
| Partition member invariant | `fc.string()` user IDs | `addMember(u)` then `removeMember(u)` restores original member count |
| Key ring wrap/unwrap round-trip | `fc.uint8Array({ minLength: 16, maxLength: 32 })` keys | `unwrap(wrap(masterKey, dataKey))` is byte-identical to `dataKey` |
| Envelope encrypt/decrypt invertibility | `fc.uint8Array()` data, `fc.uint8Array(32)` key | `envelopeDecrypt(key, envelopeEncrypt(key, data))` is byte-identical to `data` |
| Transfer rewrap correctness | `fc.uint8Array(32)` keys | `rewrapKey(oldKEK, newKEK, wrapKey(oldKEK, DEK))` unwraps to original DEK under `newKEK` |
### Load/stress test criteria
| Test | Scenario | Pass criteria |
|---|---|---|
| `grant_check_100k` | 100 000 `grantManager.check` calls | Duration < 500 ms, no memory growth |
| `token_validate_100k` | 100 000 `tokenManager.validate` calls | Duration < 500 ms, no memory growth |
| `key_ring_10k_keys` | `addKey` × 10 000 then `getKey` × 10 000 | Duration < 2 s, memory < 20 MiB |
| `envelope_encrypt_100mb` | Encrypt 100 MiB in 1 MiB chunks | Throughput ≥ 100 MiB/sec |
| `transfer_10gb` | Cross-partition transfer of 10 GiB | Throughput ≥ 50 MiB/sec, no key leaks |
| `partition_create_10k` | Create 10 000 partitions | Duration < 5 s, memory < 32 MiB |
### Security audit checklist
- [ ] All crypto operations use injectable `CryptoProvider`; no `globalThis.crypto.subtle` direct calls outside `WebCryptoProvider`.
- [ ] All async operations accept and honour `AbortSignal`.
- [ ] No plaintext key material (master keys, data keys, wrapping keys) in logs, errors, or event payloads.
- [ ] All key comparisons use constant-time equality.
- [ ] AEAD nonces are unique per envelope encryption operation (verified by fast-check collision test).
- [ ] AAD is mandatory for all envelope encryption operations.
- [ ] Token and grant integrity is verified before trust is granted.
- [ ] `DriveShareError` redacts sensitive fields before serialisation.
- [ ] Key rotation atomically replaces wrapped keys; no window where old and new keys coexist in plaintext.
- [ ] Circuit breaker and retry logic do not leak timing information about backend or key-ring state.
- [ ] Cross-partition transfer does not leave plaintext file keys in intermediate storage.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/drive` (`workspace:^`) | `Drive` — the underlying virtual filesystem |
| `@suaveplan/error` (`workspace:^`) | Base class for `DriveShareError`; `defineCodes` for `DRIVE_SHARE.*` codes |
| `@suaveplan/types` (`workspace:^`) | `Clock`, `Rng`, `LoggerLike`, `EventBusLike` |
| `@suaveplan/crypto` (`workspace:^`) | AEAD, sign, agree — used in envelope encryption and key ring |
| `@suaveplan/crypto-keys` (`workspace:^`) | `KeyPair`, `Keychain` — used in key ring |
| `@suaveplan/events` (`workspace:^`) | `EventBus` for key ring and partition lifecycle events |
| `zod` (`catalog:forms`) | Zod schemas for all public option types |
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on all externally-visible operations |
| `tslib` (`catalog:build`) | Runtime helpers |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing workspace:^`.
Note: the main branch imports `WebCryptoProvider` from within the package using `globalThis.crypto.subtle`. Genesis keeps this approach — the package exports `WebCryptoProvider` and callers may substitute an alternative `CryptoProvider` implementation.
---
## Preserved public API surface (from main branch)
### Envelope encryption
```ts
// CryptoProvider interface (caller-injectable)
interface CryptoProvider {
encrypt(key: Uint8Array, data: Uint8Array): Promise<EncryptedBundle>
decrypt(key: Uint8Array, bundle: EncryptedBundle): Promise<Uint8Array>
wrapKey(wrappingKey: Uint8Array, keyToWrap: Uint8Array): Promise<WrappedKey>
unwrapKey(wrappingKey: Uint8Array, wrappedKey: WrappedKey): Promise<Uint8Array>
generateKey(length?: number): Promise<Uint8Array>
computeIntegrity(data: Uint8Array): Promise<string>
}
// Built-in WebCrypto implementation
class WebCryptoProvider implements CryptoProvider
function envelopeEncrypt(key: Uint8Array, data: Uint8Array, options?: EnvelopeEncryptOptions): Promise<EncryptedBundle>
function envelopeDecrypt(key: Uint8Array, bundle: EncryptedBundle): Promise<Uint8Array>
function wrapKey(wrappingKey: Uint8Array, keyToWrap: Uint8Array, options?: KeyWrapOptions): Promise<WrappedKey>
function unwrapKey(wrappingKey: Uint8Array, wrappedKey: WrappedKey): Promise<Uint8Array>
function rewrapKey(oldWrappingKey: Uint8Array, newWrappingKey: Uint8Array, wrappedKey: WrappedKey): Promise<WrappedKey>
function computeIntegrity(data: Uint8Array): Promise<string>
function verifyIntegrity(data: Uint8Array, expected: string): Promise<boolean>
interface EncryptedBundle { ciphertext: Uint8Array; nonce: Uint8Array; tag?: Uint8Array }
interface EnvelopeEncryptOptions { aad?: Uint8Array; rng?: Rng }
interface KeyWrapOptions { provider?: CryptoProvider }
interface UnwrappedKey { key: Uint8Array; algorithm: string }
```
### Key ring
```ts
class KeyRing {
constructor(config?: KeyRingConfig)
addKey(userId: UserId, key: Uint8Array, options?: AddKeyOptions): Promise<void>
getKey(userId: UserId): Promise<Uint8Array | undefined>
removeKey(userId: UserId): Promise<void>
listKeys(): UserId[]
rotateKey(userId: UserId, newKey: Uint8Array): Promise<UserWrappedKey>
hasKey(userId: UserId): boolean
export(): UserWrappedKey[]
import(keys: UserWrappedKey[]): Promise<void>
}
interface KeyRingConfig { masterKey?: Uint8Array; provider?: CryptoProvider; events?: EventBusLike }
interface KeyRingEntry { userId: UserId; wrappedKey: WrappedKey; addedAt: number }
interface UserWrappedKey { userId: UserId; wrappedKey: WrappedKey }
type KeyRingEvents = { 'key:added': KeyRingEntry; 'key:removed': { userId: UserId }; 'key:rotated': { userId: UserId } }
interface AddKeyOptions { wrapWith?: Uint8Array }
```
### Partitions
```ts
class PartitionManager {
constructor(config: PartitionManagerConfig)
create(options: CreatePartitionOptions): Promise<Partition>
get(id: PartitionId): Promise<Partition | undefined>
list(): Promise<Partition[]>
delete(id: PartitionId): Promise<void>
addMember(partitionId: PartitionId, userId: UserId, options?: AddMemberOptions): Promise<void>
removeMember(partitionId: PartitionId, userId: UserId): Promise<void>
update(id: PartitionId, options: UpdatePartitionOptions): Promise<Partition>
}
interface PartitionManagerConfig { store: PartitionStore; keyRing: KeyRing; policy?: PartitionPolicyConfig; clock?: Clock; events?: EventBusLike }
interface CreatePartitionOptions { name: string; type: PartitionType; ownerId: UserId; policy?: Partial<PartitionPolicyConfig> }
interface AddMemberOptions { permissions?: SharePermission[]; wrappedKey?: WrappedKey }
interface UpdatePartitionOptions { name?: string; policy?: Partial<PartitionPolicyConfig> }
type PartitionManagerEvents = { 'partition:created': Partition; 'partition:deleted': { id: PartitionId }; 'member:added': { partitionId: PartitionId; userId: UserId }; 'member:removed': { partitionId: PartitionId; userId: UserId } }
```
### Partition store
```ts
interface PartitionStore {
get(id: PartitionId): Promise<Partition | undefined>
set(partition: Partition): Promise<void>
delete(id: PartitionId): Promise<void>
list(): Promise<Partition[]>
}
class InMemoryPartitionStore implements PartitionStore
```
### Partition policy
```ts
function createPolicy(config: Partial<PartitionPolicyConfig>): PartitionPolicyConfig
function getDefaultPolicy(): PartitionPolicyConfig
function mergePolicies(base: PartitionPolicyConfig, override: Partial<PartitionPolicyConfig>): PartitionPolicyConfig
function validateAction(policy: PartitionPolicyConfig, ctx: PolicyValidationContext): PolicyValidationResult
function requiresEncryption(policy: PartitionPolicyConfig): boolean
function requiresReauth(policy: PartitionPolicyConfig, action: PolicyAction): boolean
const DEFAULT_PRESETS: Record<string, PartitionPolicyConfig>
type PolicyAction = 'read' | 'write' | 'delete' | 'share' | 'admin'
type PolicyPresets = 'public' | 'private' | 'shared' | 'encrypted'
interface PartitionPolicyConfig { encryption: boolean; reauthRequired: PolicyAction[]; allowedActions: PolicyAction[]; maxMembers?: number; ttl?: number }
interface PolicyValidationContext { action: PolicyAction; userId: UserId; partition: Partition }
interface PolicyValidationResult { allowed: boolean; reason?: string }
```
### Sharing grants
```ts
class GrantManager {
constructor()
create(options: CreateGrantOptions): Grant
check(grantId: GrantId, userId: UserId, action: PolicyAction): GrantCheckResult
revoke(grantId: GrantId): void
list(query?: GrantQuery): Grant[]
get(grantId: GrantId): Grant | undefined
}
interface CreateGrantOptions { grantorId: UserId; granteeId: UserId; partitionId: PartitionId; permissions: SharePermission[]; expiresAt?: number }
interface Grant { id: GrantId; grantorId: UserId; granteeId: UserId; partitionId: PartitionId; permissions: SharePermission[]; createdAt: number; expiresAt?: number; revoked: boolean }
interface GrantCheckResult { allowed: boolean; reason?: string }
interface GrantQuery { granteeId?: UserId; partitionId?: PartitionId; active?: boolean }
```
### Share tokens
```ts
class TokenManager {
constructor(config?: TokenManagerConfig)
create(options: CreateTokenOptions): ShareToken
validate(tokenId: TokenId): TokenValidationResult
revoke(tokenId: TokenId): void
list(): ShareToken[]
}
interface TokenManagerConfig { clock?: Clock; defaultTTL?: number }
interface CreateTokenOptions { partitionId: PartitionId; createdBy: UserId; permissions: SharePermission[]; expiresIn?: number }
interface ShareToken { id: TokenId; partitionId: PartitionId; createdBy: UserId; permissions: SharePermission[]; createdAt: number; expiresAt: number; revoked: boolean }
interface TokenValidationResult { valid: boolean; token?: ShareToken; reason?: string }
```
### Transfer
```ts
class TransferManager {
constructor(config: TransferConfig)
transfer(options: TransferOptions): Promise<TransferResult>
getRecord(id: string): TransferRecord | undefined
listRecords(): TransferRecord[]
}
type KekResolver = (userId: UserId, partitionId: PartitionId) => Promise<Uint8Array>
type PolicyResolver = (partitionId: PartitionId) => Promise<PartitionPolicyConfig>
type FileKeyStore = Map<FileId, Uint8Array>
interface TransferConfig { kekResolver: KekResolver; policyResolver: PolicyResolver; fileKeyStore?: FileKeyStore; crypto?: CryptoProvider }
interface TransferOptions { fileId: FileId; sourcePartitionId: PartitionId; targetPartitionId: PartitionId; userId: UserId; direction: TransferDirection }
type TransferDirection = 'export' | 'import'
interface TransferRecord { id: string; fileId: FileId; sourcePartitionId: PartitionId; targetPartitionId: PartitionId; userId: UserId; direction: TransferDirection; completedAt: number; success: boolean }
interface TransferResult { success: boolean; record: TransferRecord; rewrappedKey?: WrappedKey }
```
### Common types
```ts
type UserId = Brand<string, 'UserId'>
type PartitionId = Brand<string, 'PartitionId'>
type GrantId = Brand<string, 'GrantId'>
type TokenId = Brand<string, 'TokenId'>
type FileId = Brand<string, 'FileId'>
type KeyId = Brand<string, 'KeyId'>
type PartitionType = 'personal' | 'shared' | 'public' | 'encrypted'
type SharePermission = 'read' | 'write' | 'delete' | 'share' | 'admin'
type EncryptionAlgorithm = 'AES-GCM' | 'AES-KW' | 'XChaCha20-Poly1305'
type Brand<T, B> = T & { readonly __brand: B }
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }
type WrappedKey = { algorithm: EncryptionAlgorithm; data: Uint8Array }
interface FileEncryptionMetadata { fileId: FileId; keyId: KeyId; algorithm: EncryptionAlgorithm; partitionId: PartitionId }
interface Partition { id: PartitionId; name: string; type: PartitionType; ownerId: UserId; members: PartitionMember[]; policy: PartitionPolicyConfig; createdAt: number; updatedAt: number }
interface PartitionMember { userId: UserId; permissions: SharePermission[]; addedAt: number }
interface AccessCheckResult { allowed: boolean; reason?: string }
// ID factory functions
function userId(id: string): UserId
function partitionId(id: string): PartitionId
function grantId(id: string): GrantId
function tokenId(id: string): TokenId
function fileId(id: string): FileId
function keyId(id: string): KeyId
function generateId(): string
function ok<T>(value: T): Result<T>
function err<E>(error: E): Result<never, E>
```
### Error codes
```ts
type DriveShareErrorCode =
| 'SHARE_PARTITION_NOT_FOUND'
| 'SHARE_PARTITION_EXISTS'
| 'SHARE_MEMBER_NOT_FOUND'
| 'SHARE_GRANT_NOT_FOUND'
| 'SHARE_GRANT_REVOKED'
| 'SHARE_TOKEN_EXPIRED'
| 'SHARE_TOKEN_REVOKED'
| 'SHARE_PERMISSION_DENIED'
| 'SHARE_ENCRYPTION_FAILED'
| 'SHARE_KEY_NOT_FOUND'
| 'SHARE_TRANSFER_FAILED'
| 'SHARE_POLICY_VIOLATION'
```
---
## New additions for genesis
### 1. Structured `DriveShareError`
`DriveShareError` extends `SuaveplanError` from `@suaveplan/error` carrying a `DriveShareErrorCode` discriminant. All throw sites use named factory helpers.
### 2. Zod schemas
- `PartitionManagerConfigSchema`
- `CreatePartitionOptionsSchema`
- `AddMemberOptionsSchema`
- `CreateGrantOptionsSchema`
- `CreateTokenOptionsSchema`
- `TokenManagerConfigSchema`
- `TransferOptionsSchema`
- `PartitionPolicyConfigSchema`
- `KeyRingConfigSchema`
### 3. Injectable clock and RNG
`TokenManager` and `PartitionManager` accept `clock?: Clock`. All calls to `Date.now()` are replaced by `clock.now()`. `generateId` accepts an optional `rng?: () => string` (defaults to `crypto.randomUUID`).
### 4. OTel instrumentation
| Operation | Signal |
|---|---|
| `partitionManager.create` / `delete` | span `share.partition.create` / `delete` |
| `partitionManager.addMember` | counter `share.member.add_total` |
| `grantManager.create` / `check` | span `share.grant.create`, counter `share.grant.check_total` |
| `tokenManager.create` / `validate` | span `share.token.create`, counter `share.token.validate_total` |
| `transferManager.transfer` | span `share.transfer`, histogram `share.transfer_duration_ms` |
| `keyRing.addKey` / `rotateKey` | counter `share.key.add_total`, span `share.key.rotate` |
### 5. AbortSignal on transfer
`TransferOptions` gains an optional `signal?: AbortSignal`. Long-running re-encryption operations abort on signal and throw `DriveShareError(SHARE_TRANSFER_FAILED)`.
### 6. Property-based tests (fast-check)
- **Grant expiry**: for grants with TTL T, after advancing clock by T+1, `grantManager.check` returns `allowed: false`.
- **Token revocation finality**: once `tokenManager.revoke(id)` is called, `validate(id).valid` is always false regardless of subsequent calls.
- **Policy merge commutativity for non-conflicting fields**: `mergePolicies(a, b)` for non-overlapping policy fields is equivalent to object spread.
- **Partition member invariant**: `addMember` then `removeMember` leaves the partition with the same member count as before both calls.
### 7. Benchmark suite
- `grant_check_10k`: 10 000 `grantManager.check` calls (in-memory, no IO).
- `token_validate_10k`: 10 000 `tokenManager.validate` calls.
- `key_ring_100_keys`: `keyRing.addKey` × 100 then `getKey` × 100.
- `envelope_encrypt_decrypt_10k_bytes`: encrypt + decrypt a 10 KiB buffer.
---
## File layout
```text
packages/drive/drive-sharing/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts # Public barrel
error-codes.ts # DRIVE_SHARE.* codes
constants/
index.ts # DEFAULT_TOKEN_TTL, DEFAULT_GRANT_TTL
types/
common.ts # Branded types, Result, all common interfaces
common.md
schemas.ts # Zod schemas
schemas.md
encryption/
envelope/
envelope.ts # envelopeEncrypt/Decrypt, wrapKey/unwrapKey, rewrapKey, etc.
envelope.md
envelope.types.ts # CryptoProvider, EncryptedBundle, etc.
envelope.test.ts
index.ts
key-ring/
key-ring.ts # KeyRing class
key-ring.md
key-ring.types.ts
key-ring.test.ts
index.ts
partitions/
partition-manager/
partition-manager.ts
partition-manager.md
partition-manager.types.ts
partition-manager.test.ts
index.ts
partition-policy/
partition-policy.ts # createPolicy, validateAction, DEFAULT_PRESETS, etc.
partition-policy.md
partition-policy.types.ts
partition-policy.test.ts
index.ts
partition-store/
partition-store.ts # InMemoryPartitionStore
partition-store.md
partition-store.types.ts
partition-store.test.ts
index.ts
sharing/
grants/
grants.ts # GrantManager
grants.md
grants.types.ts
grants.test.ts
index.ts
tokens/
tokens.ts # TokenManager
tokens.md
tokens.types.ts
tokens.test.ts
index.ts
transfer/
transfer/
transfer.ts # TransferManager
transfer.md
transfer.types.ts
transfer.test.ts
index.ts
__benchmarks__/
drive-sharing.bench.ts
tests/
integration.test.ts # End-to-end: create partition, add member, grant, transfer
property.test.ts # fast-check property tests
```
---
## Impact
**Downstream packages**: `@suaveplan/drive-react-sharing` (not yet on genesis) will depend on this. No existing genesis package depends on it.
**Breaking changes from main-branch API**: none. All main-branch call sites compile unchanged. New optional fields (`clock`, `rng`, `signal`) and error codes are additive.
**Workspace dep prerequisites**: `@suaveplan/drive`, `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/crypto`, `@suaveplan/crypto-keys`, `@suaveplan/events` must be published to Verdaccio before this package can be built or tested.
---
## Source mapping
> **REQUIRED for any package migrated from `../suaveplan/`.**
This change derives from `../suaveplan/enterprise/drive-sharing.md`.
| Requirement | Source lines | Notes |
|---|---|---|
| Structured DriveShareError hierarchy | L42, L61 | verbatim — structured errors extending `@suaveplan/error` |
| Zod schemas for all public option types | L38, L60 | verbatim — Zod schemas for all public types; public API stability |
| Injectable clock and RNG | L40 | verbatim — deterministic behaviour; no `Math.random()` / `Date.now()` at module scope |
| OpenTelemetry instrumentation on all externally-visible operations | L41, L70 | verbatim — OTel for all public entrypoints; observability surface |
| AbortSignal on cross-partition transfer | L43 | reworded — backpressure & cancellation; `AbortSignal` everywhere async, applied specifically to `TransferManager.transfer` |
| CryptoProvider interface and WebCryptoProvider | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Envelope encryption and decryption | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Key wrap, unwrap, and rewrap | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Key ring management | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Partition policy creation and validation | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Partition store interface | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Partition manager CRUD and membership | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Grant manager create, check, and revoke | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Token manager create, validate, and revoke with TTL | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Cross-partition file transfer | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Branded ID utilities | — | derived from main-branch API surface preserved in `proposal.md`; no explicit line in enterprise spec |
| Property-based test invariants | L39 | verbatim — property-based tests (fast-check) for pure functions and state machines |
| Benchmark suite coverage | L72 | reworded — benchmark suite in `src/__benchmarks__` with published numbers |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| CryptoProvider interface and WebCryptoProvider | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface); enterprise spec does not enumerate methods individually |
| Envelope encryption and decryption | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Key wrap, unwrap, and rewrap | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Key ring management | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Partition policy creation and validation | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Partition store interface | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Partition manager CRUD and membership | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Grant manager create, check, and revoke | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Token manager create, validate, and revoke with TTL | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Cross-partition file transfer | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Branded ID utilities | Preserved public API surface from main branch (see `proposal.md` § Preserved public API surface) |
| Benchmark suite coverage | Genesis adds mitata benchmark suite and CI regression gate beyond main branch baseline |