6.8 KiB
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 fromImageData,Uint8Array,Blob, orFile - ADDED:
src/scan/scan-camera.ts—scanFromCameraStream(stream, options?)extracts frames from aMediaStreamand attempts decode - ADDED:
src/scan/totp-extract.ts—extractTotpUri(data)parsesotpauth://totp/URIs into structured config - ADDED:
src/scan/totp-types.ts—TotpConfig,TotpAlgorithm,OtpAuthUri - ADDED: Subpath export
./scan/scaninpackage.json - ADDED:
QrScanErrorextendsQrError(codeQRCODE.DECODE_FAILED) - ADDED:
CameraAccessErrorextendsQrScanError(codeQRCODE.CAMERA_ACCESS_DENIED) - ADDED:
TotpUriParseErrorextendsQrScanError(codeQRCODE.TOTP_PARSE_ERROR) - ADDED: Injectable
clock?: ClockinscanFromCameraStreamoptions for deterministic frame-interval timing in tests - ADDED: Zod schemas for
TotpConfigand camera options - ADDED: OTel counter
qrcode.scan.total(bysource,outcome) and histogramqrcode.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 returningTotpConfig; unknown URI schemes rejected explicitly. Rejectsjavascript: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.totalbysourceandoutcome; histogramqrcode.scan.duration_ms; gauge for camera stream active duration. - Structured logging: JSON logs at
warnfor camera permission denial,errorfor decode failures,debugfor 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, andextractTotpUriare 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
MediaStreamcleanup 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 errorsrc/scan/scan-camera.test.ts— covers frame extraction, permission denial, stream endedsrc/scan/totp-extract.test.ts— covers valid otpauth URI, missing secret, malformed URIsrc/scan/scan.spec-conformance.test.ts— pins type-level contracts, strict-mode behaviors, OTel emissiontests/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