Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,779 @@
# 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 |