# 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` — 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 decrypt(key: Uint8Array, bundle: EncryptedBundle): Promise wrapKey(wrappingKey: Uint8Array, keyToWrap: Uint8Array): Promise unwrapKey(wrappingKey: Uint8Array, wrappedKey: WrappedKey): Promise generateKey(length?: number): Promise computeIntegrity(data: Uint8Array): Promise } // Built-in WebCrypto implementation class WebCryptoProvider implements CryptoProvider function envelopeEncrypt(key: Uint8Array, data: Uint8Array, options?: EnvelopeEncryptOptions): Promise function envelopeDecrypt(key: Uint8Array, bundle: EncryptedBundle): Promise function wrapKey(wrappingKey: Uint8Array, keyToWrap: Uint8Array, options?: KeyWrapOptions): Promise function unwrapKey(wrappingKey: Uint8Array, wrappedKey: WrappedKey): Promise function rewrapKey(oldWrappingKey: Uint8Array, newWrappingKey: Uint8Array, wrappedKey: WrappedKey): Promise function computeIntegrity(data: Uint8Array): Promise function verifyIntegrity(data: Uint8Array, expected: string): Promise 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 getKey(userId: UserId): Promise removeKey(userId: UserId): Promise listKeys(): UserId[] rotateKey(userId: UserId, newKey: Uint8Array): Promise hasKey(userId: UserId): boolean export(): UserWrappedKey[] import(keys: UserWrappedKey[]): Promise } 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 get(id: PartitionId): Promise list(): Promise delete(id: PartitionId): Promise addMember(partitionId: PartitionId, userId: UserId, options?: AddMemberOptions): Promise removeMember(partitionId: PartitionId, userId: UserId): Promise update(id: PartitionId, options: UpdatePartitionOptions): Promise } interface PartitionManagerConfig { store: PartitionStore; keyRing: KeyRing; policy?: PartitionPolicyConfig; clock?: Clock; events?: EventBusLike } interface CreatePartitionOptions { name: string; type: PartitionType; ownerId: UserId; policy?: Partial } interface AddMemberOptions { permissions?: SharePermission[]; wrappedKey?: WrappedKey } interface UpdatePartitionOptions { name?: string; policy?: Partial } 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 set(partition: Partition): Promise delete(id: PartitionId): Promise list(): Promise } class InMemoryPartitionStore implements PartitionStore ``` ### Partition policy ```ts function createPolicy(config: Partial): PartitionPolicyConfig function getDefaultPolicy(): PartitionPolicyConfig function mergePolicies(base: PartitionPolicyConfig, override: Partial): PartitionPolicyConfig function validateAction(policy: PartitionPolicyConfig, ctx: PolicyValidationContext): PolicyValidationResult function requiresEncryption(policy: PartitionPolicyConfig): boolean function requiresReauth(policy: PartitionPolicyConfig, action: PolicyAction): boolean const DEFAULT_PRESETS: Record 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 getRecord(id: string): TransferRecord | undefined listRecords(): TransferRecord[] } type KekResolver = (userId: UserId, partitionId: PartitionId) => Promise type PolicyResolver = (partitionId: PartitionId) => Promise type FileKeyStore = Map 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 type PartitionId = Brand type GrantId = Brand type TokenId = Brand type FileId = Brand type KeyId = Brand type PartitionType = 'personal' | 'shared' | 'public' | 'encrypted' type SharePermission = 'read' | 'write' | 'delete' | 'share' | 'admin' type EncryptionAlgorithm = 'AES-GCM' | 'AES-KW' | 'XChaCha20-Poly1305' type Brand = T & { readonly __brand: B } type Result = { 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(value: T): Result function err(error: E): Result ``` ### 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 |