Files
2026-05-27 23:29:53 +01:00

103 lines
6.8 KiB
Markdown

# OpenSpec Change: qrcode-scan
**Package**: `@suaveplan/qrcode` (adds `/scan` subpath)
**Status**: PROPOSED
**Type**: feature
## Why
`@suaveplan/qrcode` is currently generation-only. Vaulted needs to import TOTP secrets by scanning QR codes from camera streams or image uploads. Rather than creating a standalone package that would duplicate QR matrix data structures, the decision is to extend the existing `@suaveplan/qrcode` package with a tree-shakeable `/scan` subpath.
## What changes
- ADDED: `src/scan/scan.ts` — `scanFromImageData(imageData)` decodes a QR code from `ImageData`, `Uint8Array`, `Blob`, or `File`
- ADDED: `src/scan/scan-camera.ts` — `scanFromCameraStream(stream, options?)` extracts frames from a `MediaStream` and attempts decode
- ADDED: `src/scan/totp-extract.ts` — `extractTotpUri(data)` parses `otpauth://totp/` URIs into structured config
- ADDED: `src/scan/totp-types.ts` — `TotpConfig`, `TotpAlgorithm`, `OtpAuthUri`
- ADDED: Subpath export `./scan/scan` in `package.json`
- ADDED: `QrScanError` extends `QrError` (code `QRCODE.DECODE_FAILED`)
- ADDED: `CameraAccessError` extends `QrScanError` (code `QRCODE.CAMERA_ACCESS_DENIED`)
- ADDED: `TotpUriParseError` extends `QrScanError` (code `QRCODE.TOTP_PARSE_ERROR`)
- ADDED: Injectable `clock?: Clock` in `scanFromCameraStream` options for deterministic frame-interval timing in tests
- ADDED: Zod schemas for `TotpConfig` and camera options
- ADDED: OTel counter `qrcode.scan.total` (by `source`, `outcome`) and histogram `qrcode.scan.duration_ms`
## Out of scope
- Real-time continuous scanning UI components (Vaulted app concern, not library)
- Barcode formats other than QR (use `@suaveplan/barcode`)
- Video recording, snapshot capture, or `<canvas>` rendering helpers (consume browser APIs directly)
- Native camera integration on mobile (Capacitor Camera plugin is a separate abstraction layer)
## Gap analysis: sub-standard → enterprise
Most QR scanning libraries in the npm ecosystem pull in heavy WebAssembly binaries or camera UI widgets that bloat bundles and tie the code to browser-only environments. They often lack frame-rate throttling, leak `MediaStream` resources on unmount, and provide no structured extraction for TOTP URIs. Enterprise-grade code must keep the decoder lightweight and tree-shakeable, manage camera lifecycle explicitly, and return strongly-typed TOTP config rather than raw strings.
## Security model
- **Threat model**: Malicious QR payloads exploiting URI parser bugs to inject JavaScript or redirect to phishing domains; camera stream frames intercepted by compromised browser extensions; resource exhaustion via high-frequency decode loops.
- **Privacy guarantees**: Camera frames are processed in-memory and never persisted to disk or transmitted. TOTP secrets extracted from QR codes are returned to the consumer without logging or retention.
- **Input validation**: `otpauth://` URIs validated against strict Zod schema before returning `TotpConfig`; unknown URI schemes rejected explicitly. Rejects `javascript:` and other non-`otpauth:` schemes in parsed URIs.
- **Side-channel resistance**: Decode timing is not correlated with secret content; constant-time URI component extraction where feasible.
- **Key material handling**: Extracted TOTP secrets are handled as opaque strings and passed immediately to the consumer; no key derivation or storage occurs in this package.
## Performance SLOs
- **Latency**: P95 single-frame decode < 100ms on a mid-tier laptop; camera stream first detection < 2s.
- **Throughput**: Sustained 30 fps frame sampling without dropping frames or blocking the main thread.
- **Memory bounds**: Peak memory per decode < 2 MB; camera stream buffer bounded by frame queue depth.
- **Bundle size budget**: Scan subpath < 25 kB gzipped (decoder + TOTP extractor); no WASM or heavy CV dependencies.
## Observability plan
- **OTel spans/metrics**: Counter `qrcode.scan.total` by `source` and `outcome`; histogram `qrcode.scan.duration_ms`; gauge for camera stream active duration.
- **Structured logging**: JSON logs at `warn` for camera permission denial, `error` for decode failures, `debug` for successful scans.
- **Error telemetry**: Distinct codes (`CAMERA_ACCESS_DENIED`, `TOTP_PARSE_ERROR`) with platform context and frame source.
- **Health checks**: Expose `scanFromCameraStream.isHealthy()` returning false after repeated decode failures or stream errors.
## Dependency audit
- **Runtime deps**: `@suaveplan/qrcode` (shared matrix types), `@suaveplan/errors`, `@suaveplan/otel`; decoder is pure TypeScript with no heavy CV libraries.
- **Supply-chain risk**: No WASM binaries or native modules; all dependencies are first-party or platform APIs.
- **Tree-shakeability**: `scanFromImageData`, `scanFromCameraStream`, and `extractTotpUri` are individually tree-shakeable.
- **License**: All code MIT-licensed; no copyleft dependencies.
## Operational readiness
- **Graceful degradation**: If camera access is denied, the consumer can fall back to file upload. If decode fails, return a typed error rather than throwing.
- **Resource limits**: Frame extraction rate limited by configurable throttle; max decode attempts per stream bounded.
- **Error recovery**: Automatic `MediaStream` cleanup on unmount or error; retry with backoff on transient camera glitches.
- **Backpressure**: Frame queue drops oldest frames when decoder is slower than capture rate; prevents unbounded memory growth.
- **Version compatibility**: Public API frozen after v1; additive-only changes; deprecated APIs supported for two major versions.
## Verification approach
- **100% coverage target**: All exported functions, branches, and error paths instrumented in test.
- **Property-based tests**: QR payload generation, URI parsing edge cases, and frame buffer backpressure tested with fast-check invariants.
- **Load/stress criteria**: Sustained 30 fps decode for 60s without memory growth > 10% or frame drops > 1%.
- **Security audit checklist**: Malformed URI injection, oversized image payload handling, camera stream cleanup on error, secret leakage in logs.
- `src/scan/scan.test.ts` — covers decode from ImageData, Uint8Array, File, no-QR error
- `src/scan/scan-camera.test.ts` — covers frame extraction, permission denial, stream ended
- `src/scan/totp-extract.test.ts` — covers valid otpauth URI, missing secret, malformed URI
- `src/scan/scan.spec-conformance.test.ts` — pins type-level contracts, strict-mode behaviors, OTel emission
- `tests/dep-graph.test.ts` — pins dependency invariants
## Migration impact
None — additive only. Existing `@suaveplan/qrcode` consumers are unaffected because all new code is behind the `./scan/scan` subpath export.
## Impact
### Downstream consumers
Run `bunx turbo ls --filter=...@suaveplan/qrcode` to enumerate.
### Breaking changes
None — additive only.
### Workspace dep prerequisites
- `@suaveplan/errors`
- `@suaveplan/otel`