Initial Commit
This commit is contained in:
779
openspec/archive/2026-05-04-enterprise-drive-sharing/proposal.md
Normal file
779
openspec/archive/2026-05-04-enterprise-drive-sharing/proposal.md
Normal 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 |
|
||||
Reference in New Issue
Block a user