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

44 KiB
Raw Blame History

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

// 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

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

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

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

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

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

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

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

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

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

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