6.5 KiB
Capability Spec Delta: qrcode-scan
Package: @suaveplan/qrcode (/scan subpath)
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions.
ADDED Requirements
Requirement: Image decode from multiple input types
scanFromImageData(input) MUST accept ImageData, Uint8Array, Blob, or File. It MUST decode the QR code matrix and return the raw payload string. If no QR code is found, QrScanError (code QRCODE.DECODE_FAILED) MUST be thrown.
Scenario: Decode from ImageData
- WHEN
scanFromImageDatareceives anImageDataobject containing a valid QR code - THEN it returns the decoded string payload
Scenario: Decode from File
- WHEN
scanFromImageDatareceives aFileobject containing a valid QR code - THEN it returns the decoded string payload
Scenario: No QR code found
- WHEN
scanFromImageDatareceives an image with no QR code - THEN it throws
QrScanErrorwith codeQRCODE.DECODE_FAILED
Verified by: src/scan/scan.test.ts::"decodes QR code from ImageData" AND src/scan/scan.test.ts::"decodes QR code from File" AND src/scan/scan.test.ts::"throws QrScanError when no QR code is found"
Requirement: Camera stream access
scanFromCameraStream(stream, options?) MUST accept a MediaStream and extract video frames at a configurable interval (default 250 ms). It MUST attempt to decode a QR code from each frame. On first successful decode, it MUST return the payload and stop frame extraction. If options.signal aborts, it MUST reject with an abort error. If the stream ends without a successful decode, it MUST throw QrScanError (code QRCODE.DECODE_FAILED).
Scenario: Successful decode from camera stream
- WHEN
scanFromCameraStreamreceives a stream where one frame contains a valid QR code - THEN it returns the payload after processing that frame
Scenario: Abort signal stops scanning
- WHEN
options.signalaborts before a QR code is found - THEN the promise rejects with an abort error
Scenario: Stream ends without decode
- WHEN the stream ends and no frame contained a QR code
- THEN it throws
QrScanErrorwith codeQRCODE.DECODE_FAILED
Verified by: src/scan/scan-camera.test.ts::"returns payload when QR code appears in stream" AND src/scan/scan-camera.test.ts::"rejects when abort signal fires before decode" AND src/scan/scan-camera.test.ts::"throws QrScanError when stream ends without finding QR code"
Requirement: Injectable Clock for deterministic camera timing
scanFromCameraStream options MUST include an optional clock?: Clock. When provided, frame extraction intervals and timeout deadlines MUST be driven by the injected clock instead of the global time source.
Verified by: src/scan/scan-camera.test.ts::"uses injected clock for frame intervals" AND src/scan/scan-camera.test.ts::"uses injected clock for stream timeout"
Requirement: TOTP URI extraction
extractTotpUri(data) MUST parse otpauth://totp/ URIs per the de-facto Google Authenticator Key URI Format. It MUST return a TotpConfig with:
secret(required) — base32-encoded secretissuer(optional) — issuer nameaccount(optional) — account namealgorithm(optional, defaultSHA1) —SHA1 | SHA256 | SHA512digits(optional, default6) —6 | 7 | 8period(optional, default30) — seconds as integer
If secret is missing or the URI scheme is not otpauth://totp/, TotpUriParseError (code QRCODE.TOTP_PARSE_ERROR) MUST be thrown with context.rawUri populated.
Scenario: Valid TOTP URI
- WHEN
extractTotpUrireceives"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" - THEN it returns a
TotpConfigwith all fields correctly populated
Scenario: Missing secret
- WHEN
extractTotpUrireceives"otpauth://totp/Example:alice@google.com?issuer=Example" - THEN it throws
TotpUriParseError
Scenario: Invalid scheme
- WHEN
extractTotpUrireceives"https://example.com" - THEN it throws
TotpUriParseError
Verified by: src/scan/totp-extract.test.ts::"parses valid otpauth URI into TotpConfig" AND src/scan/totp-extract.test.ts::"throws TotpUriParseError when secret is missing" AND src/scan/totp-extract.test.ts::"throws TotpUriParseError for non-otpauth scheme"
Requirement: Subpath export configuration
The package.json exports map MUST include an entry for ./scan/scan so consumers can import scan functionality without pulling in generation code:
"./scan/scan": {
"import": { "types": "./dist/scan/scan.d.ts", "default": "./dist/scan/scan.js" }
}
Scenario: Tree-shakeable import
- WHEN a consumer imports from
@suaveplan/qrcode/scan/scan - THEN only scan-related code is included in the bundle
Verified by: tests/dep-graph.test.ts::"scan subpath is tree-shakeable and does not import generator modules"
Requirement: Structured errors
All scan-related thrown errors MUST extend QrScanError which extends QrError. The following codes MUST be defined in the existing qrcode error code registry:
QRCODE.CAMERA_ACCESS_DENIED— camera permission was denied or stream could not be obtainedQRCODE.TOTP_PARSE_ERROR— the decoded data is not a validotpauth://totp/URI
QrScanError, CameraAccessError, and TotpUriParseError MUST be exported from the ./scan/scan subpath.
Verified by: src/scan/scan.spec-conformance.test.ts::"all scan errors extend QrScanError" AND src/scan/scan.spec-conformance.test.ts::"camera access error code is registered"
Requirement: Observability
The following metrics MUST be emitted via @opentelemetry/api using meter name @suaveplan/qrcode:
- Counter
qrcode.scan.totalwith attributessource(image/camera) andoutcome(success/error/not_found) - Histogram
qrcode.scan.duration_msrecorded on everyscanFromImageDataandscanFromCameraStreamcall
All instruments MUST be no-ops when no OTel metric exporter is configured.
Verified by: src/scan/scan.spec-conformance.test.ts::"emits scan total counter with correct attributes on success" AND src/scan/scan.spec-conformance.test.ts::"emits scan duration histogram on every call"
Requirement: 100% coverage
Vitest thresholds for the @suaveplan/qrcode package MUST remain at 100/100/100/100 after adding the scan subpath. No .skip or .only is permitted in src/scan/.
Verified by: vitest.config.ts AND src/scan/scan.test.ts (all branches covered)