Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
# 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** `scanFromImageData` receives an `ImageData` object containing a valid QR code
- **THEN** it returns the decoded string payload
#### Scenario: Decode from File
- **WHEN** `scanFromImageData` receives a `File` object containing a valid QR code
- **THEN** it returns the decoded string payload
#### Scenario: No QR code found
- **WHEN** `scanFromImageData` receives an image with no QR code
- **THEN** it throws `QrScanError` with code `QRCODE.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** `scanFromCameraStream` receives 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.signal` aborts 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 `QrScanError` with code `QRCODE.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 secret
- `issuer` (optional) — issuer name
- `account` (optional) — account name
- `algorithm` (optional, default `SHA1`) — `SHA1 | SHA256 | SHA512`
- `digits` (optional, default `6`) — `6 | 7 | 8`
- `period` (optional, default `30`) — 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** `extractTotpUri` receives `"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"`
- **THEN** it returns a `TotpConfig` with all fields correctly populated
#### Scenario: Missing secret
- **WHEN** `extractTotpUri` receives `"otpauth://totp/Example:alice@google.com?issuer=Example"`
- **THEN** it throws `TotpUriParseError`
#### Scenario: Invalid scheme
- **WHEN** `extractTotpUri` receives `"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:
```json
"./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 obtained
- `QRCODE.TOTP_PARSE_ERROR` — the decoded data is not a valid `otpauth://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.total` with attributes `source` (`image` / `camera`) and `outcome` (`success` / `error` / `not_found`)
- Histogram `qrcode.scan.duration_ms` recorded on every `scanFromImageData` and `scanFromCameraStream` call
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)