Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# `hotp/encode-counter`
|
||||
|
||||
## Purpose
|
||||
|
||||
Encode an HOTP counter as the 8-byte big-endian block required by RFC 4226
|
||||
§5.1. `bigint` is the canonical input type because `Number` loses precision
|
||||
past `2^53` and the RFC permits counters up to `2^64 − 1`. Shared by HOTP
|
||||
itself, by OCRA's optional `C` directive, and by OCRA's `T` timestamp block
|
||||
(which is also written as an 8-byte big-endian integer per RFC 6287 §5.1).
|
||||
|
||||
## Features
|
||||
|
||||
- BigInt input — no precision loss across the full 0..2^64-1 range.
|
||||
- Range guard: negative counters and counters past `2^64-1` raise
|
||||
`OtpError(OTP.INVALID_OPTIONS)`.
|
||||
- Pure: returns a freshly-allocated `Uint8Array(8)` per call.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encodeCounter } from "@suaveplan/otp";
|
||||
|
||||
const block = encodeCounter(1n);
|
||||
// block === Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encodeCounter } from "@suaveplan/otp";
|
||||
|
||||
// Counter past Number.MAX_SAFE_INTEGER round-trips losslessly.
|
||||
const block = encodeCounter((1n << 53n) + 1n);
|
||||
// `block` is exactly 8 bytes; the high bytes carry the counter's high bits.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodeCounter(counter: bigint): Uint8Array` — returns an 8-byte
|
||||
big-endian block. Negative input raises `OtpError(OTP.INVALID_OPTIONS)`;
|
||||
values >= 2^64 raise the same code.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function loops from byte index 7 back to 0 shifting `counter` right by 8
|
||||
bits each step. The mask `0xffn` keeps the operation inside the `bigint`
|
||||
domain so no implicit `Number` conversion can lose precision. The function
|
||||
is the single source of truth for big-endian counter framing — both HOTP and
|
||||
OCRA route through it so any future endianness change propagates atomically.
|
||||
|
||||
The upper-bound check `counter > MAX_U64` catches the niche case where a
|
||||
caller passes a programmatically-generated counter past the RFC 4226
|
||||
limit. Without the check the encoder would silently truncate the high
|
||||
bits and produce two distinct counters that hash to the same byte block,
|
||||
which would silently break the per-counter uniqueness invariant HOTP
|
||||
relies on. The lower-bound check on negative counters covers the
|
||||
symmetric case for callers who derive counters via `BigInt()` arithmetic
|
||||
that may go negative under bug conditions.
|
||||
@@ -0,0 +1,56 @@
|
||||
# `hotp/hmac`
|
||||
|
||||
## Purpose
|
||||
|
||||
Algorithm-routing HMAC façade for the OTP family. SHA-256 and SHA-512 route
|
||||
through `@suaveplan/crypto-hash` (the canonical platform HMAC); SHA-1 routes
|
||||
through `@noble/hashes/sha1` directly because crypto-hash deliberately does
|
||||
not expose SHA-1 — it is mandated only by legacy OTP RFCs and must not be
|
||||
presented as a general-purpose option elsewhere in the platform.
|
||||
|
||||
## Features
|
||||
|
||||
- Three algorithms: SHA-1, SHA-256, SHA-512.
|
||||
- SHA-1 isolated to the OTP family — no leakage into the wider crypto API.
|
||||
- Returns a freshly-allocated `Uint8Array` of the algorithm's digest size.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hmacFor } from "@suaveplan/otp";
|
||||
|
||||
const tag = hmacFor("SHA1", key, message);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hmacFor } from "@suaveplan/otp";
|
||||
|
||||
// Build the OCRA HMAC input directly — useful for KAT-driven test fixtures.
|
||||
const tag = hmacFor("SHA256", key, suiteAsciiPlusZeroPlusDataInput);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `hmacFor(algorithm: HotpAlgorithm, key: Uint8Array, data: Uint8Array): Uint8Array`
|
||||
— returns the HMAC tag.
|
||||
- `HotpAlgorithm = "SHA1" | "SHA256" | "SHA512"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Routing inside this module rather than at the call site keeps the SHA-1
|
||||
exception local to OTP. The crypto-hash boundary uses dashed names
|
||||
(`"SHA-256"`); this module accepts the OTP-RFC-style undashed names
|
||||
(`"SHA256"`) and translates internally. Adding a new algorithm here is the
|
||||
only change required — `hotp.ts`, `totp.ts`, and `ocra.ts` all consume this
|
||||
single dispatch point.
|
||||
|
||||
The SHA-1 routing decision is intentional: `@suaveplan/crypto-hash` is the
|
||||
canonical platform façade for hashing across the project, and exposing
|
||||
SHA-1 there would invite consumers to use it for non-OTP purposes. By
|
||||
isolating the SHA-1 dependency to the OTP module, the rest of the platform
|
||||
keeps a no-SHA-1 posture without breaking RFC 4226 / 6238 / 6287
|
||||
compliance. The noble HMAC primitive used here is the same implementation
|
||||
crypto-hash routes through under the hood for SHA-256 / SHA-512, so the
|
||||
two paths behave identically apart from the chosen hash.
|
||||
@@ -0,0 +1,79 @@
|
||||
# `hotp/hotp`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs --> - hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
|
||||
- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
|
||||
- hotp.schema — Zod boundary schema for `HotpOptions`…
|
||||
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
- [hotp.schema](./hotp.schema.md) — Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown keys (so silent typos in caller configuratio…
|
||||
|
||||
## Purpose
|
||||
|
||||
RFC 4226 HOTP — HMAC-based one-time password. Computes
|
||||
`value = DT(HMAC-H(K, C)) mod 10^digits` and returns a `digits`-length
|
||||
zero-padded decimal string. Counter is `bigint` (RFC 4226 §5.1 mandates an
|
||||
8-byte big-endian counter; `Number` loses precision past `2^53`).
|
||||
|
||||
## Features
|
||||
|
||||
- RFC 4226 dynamic truncation per §5.3 step 2.
|
||||
- SHA-1 default; SHA-256 / SHA-512 widening per RFC 6238 §1.2.
|
||||
- 6 / 7 / 8-digit output.
|
||||
- Zod-validated options at the boundary; `INVALID_DIGITS` mapped via the
|
||||
zod-map for fields that fail enum/literal-union checks.
|
||||
- Empty-secret rejection with `OTP.INVALID_SECRET`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hotp } from "@suaveplan/otp";
|
||||
|
||||
const secret = new TextEncoder().encode("12345678901234567890");
|
||||
const code = hotp(secret, 0n);
|
||||
// code === "755224" (RFC 4226 Appendix D vector)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hotp } from "@suaveplan/otp";
|
||||
|
||||
const code = hotp(secret, (1n << 53n) + 1n, {
|
||||
algorithm: "SHA512",
|
||||
digits: 8,
|
||||
});
|
||||
// 8-digit SHA-512 HOTP at a counter past Number.MAX_SAFE_INTEGER
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `hotp(secret: Uint8Array, counter: bigint, opts?: HotpOptions): string`
|
||||
— returns the `digits`-length decimal code.
|
||||
- `HotpOptions` — `{ algorithm?: "SHA1" | "SHA256" | "SHA512"; digits?: 6 | 7 | 8 }`.
|
||||
- Throws `OtpError(OTP.INVALID_OPTIONS | OTP.INVALID_DIGITS)` on Zod
|
||||
failures and `OtpError(OTP.INVALID_SECRET)` on empty `secret`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Counter encoding lives in `encode-counter.ts` and is shared with OCRA's
|
||||
`C` directive. Dynamic truncation extracts a 31-bit value from the HMAC by
|
||||
masking the high bit to avoid sign-extension surprises across HMAC
|
||||
implementations. Modular reduction uses a precomputed `POW10` table indexed
|
||||
by `digits` rather than `Math.pow` so the modulus is an exact integer.
|
||||
|
||||
The Zod schema rejects unknown keys at the strict-mode boundary so callers
|
||||
who construct options from JSON or external configuration cannot smuggle
|
||||
typo'd fields past the API. Field-level Zod failures flow through
|
||||
`mapZodToOtp` so a bad `digits` value surfaces as `OTP.INVALID_DIGITS`
|
||||
rather than the generic `OTP.INVALID_OPTIONS`. The empty-secret guard is
|
||||
runtime-only because Zod's bytes type does not constrain length.
|
||||
@@ -0,0 +1,61 @@
|
||||
# `hotp/hotp.schema`
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod boundary schema for `HotpOptions`. Strict-mode parsing rejects unknown
|
||||
keys (so silent typos in caller configuration surface immediately) and
|
||||
narrows `algorithm` and `digits` to their literal-union values. Mismatches
|
||||
flow through `mapZodToOtp` so the boundary error carries a specific code
|
||||
(`INVALID_DIGITS` for the `digits` field, `INVALID_OPTIONS` otherwise).
|
||||
|
||||
## Features
|
||||
|
||||
- `algorithm` enum: `"SHA1" | "SHA256" | "SHA512"`.
|
||||
- `digits` literal union: `6 | 7 | 8`.
|
||||
- Strict-mode object — unknown keys are rejected.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { HotpOptionsSchema } from "@suaveplan/otp";
|
||||
|
||||
HotpOptionsSchema.parse({ algorithm: "SHA1", digits: 6 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { HotpOptionsSchema } from "@suaveplan/otp";
|
||||
|
||||
// Programmatic option builder — feed parsed result directly into hotp().
|
||||
const opts = HotpOptionsSchema.parse(JSON.parse(userJson));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `HotpOptionsSchema` — Zod object schema with `.strict()`.
|
||||
- `HotpOptionsInput` — type alias for the parsed shape.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The strict-mode check is the second-line defence behind TypeScript's compile
|
||||
checks: callers using `as never` or constructing options from JSON can still
|
||||
slip values past the type system. Pinning the schema here keeps the runtime
|
||||
contract aligned with the type-level contract.
|
||||
|
||||
The schema is intentionally narrow — it covers only the surface that
|
||||
HOTP itself needs. Callers building their own option bags around HOTP
|
||||
(e.g. a vault item type that wraps HOTP with extra metadata) should
|
||||
extend this schema using Zod's `.extend()` or compose it via a discriminated
|
||||
union, rather than relaxing the strict-mode setting here. Keeping the base
|
||||
schema tight prevents accidental option-shape drift across the OTP family
|
||||
and means every caller benefits from the same validation guarantees.
|
||||
|
||||
The `algorithm` enum covers exactly the three hashes RFC 4226 / 6238 §1.2
|
||||
allow for OTP. SHA-3 family hashes are not part of any OTP RFC and would
|
||||
be rejected at this boundary even though `@noble/hashes` provides them.
|
||||
|
||||
The `digits` literal union mirrors RFC 4226 §5.3 (`6`) plus the RFC 6238
|
||||
§1.2 widening (`7`, `8`). RFC 6287 OCRA additionally permits digits=4,
|
||||
which is handled in the OCRA suite parser rather than here because the
|
||||
HOTP primitive does not need to expose it directly.
|
||||
Reference in New Issue
Block a user