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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# webcrypto-aead-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides an `AeadEngine` implementation that delegates all AES-256-GCM operations to the browser's (or Node's) native `crypto.subtle` API. This lets `@suaveplan/crypto` facade functions — `encrypt()` and `decrypt()` from the `aead` sub-module — run on hardware-accelerated paths wherever the Web Crypto API is available, instead of the default pure-JavaScript `@noble/ciphers` path.
The engine is designed for environments where minimising bundle size matters most: importing it adds zero third-party cryptography code because everything is provided by the host runtime.
## Features
- `encrypt(key, plaintext, nonce, aad?)` — AES-256-GCM encryption via `crypto.subtle.encrypt`, including automatic 16-byte authentication tag appended to ciphertext
- `decrypt(key, ciphertext, nonce, aad?)` — AES-256-GCM decryption via `crypto.subtle.decrypt` with authenticated tag verification
- Optional additional authenticated data (AAD) for binding ciphertext to a context
- Throws typed `BrowserCryptoError` with stable codes for every failure path
- Returns `Promise<Uint8Array>` — engine methods are async even though the `AeadEngine` interface declares sync return types; facade functions accept Promise results
- Guards every call with `assertWebCryptoAvailable()` to produce a structured error instead of a runtime `TypeError`
## Usage: Basic Example
```ts
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
const key = crypto.getRandomValues(new Uint8Array(32));
const nonce = crypto.getRandomValues(new Uint8Array(12));
const plaintext = new TextEncoder().encode("hello world");
const ciphertext = await (webCryptoAeadEngine.encrypt(key, plaintext, nonce) as unknown as Promise<Uint8Array>);
const recovered = await (webCryptoAeadEngine.decrypt(key, ciphertext, nonce) as unknown as Promise<Uint8Array>);
console.log(new TextDecoder().decode(recovered)); // "hello world"
```
## Usage: Advanced Example
```ts
import { encrypt } from "@suaveplan/crypto/aead";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
async function sealPayload(
key: Uint8Array,
payload: Uint8Array,
context: Uint8Array // AAD binds the ciphertext to e.g. a session ID
): Promise<Uint8Array> {
try {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const sealed = await (webCryptoAeadEngine.encrypt(key, payload, nonce, context) as unknown as Promise<Uint8Array>);
// Prepend nonce for transport: [12 bytes nonce | ciphertext+tag]
const out = new Uint8Array(12 + sealed.length);
out.set(nonce, 0);
out.set(sealed, 12);
return out;
} catch (err) {
if (err instanceof BrowserCryptoError) {
// err.code is one of: BROWSER_CRYPTO.NOT_AVAILABLE | BROWSER_CRYPTO.OPERATION.FAILED
throw err;
}
throw err;
}
}
```
## API Reference
### `webCryptoAeadEngine: AeadEngine`
An object conforming to the `AeadEngine` interface from `@suaveplan/crypto/engines`.
#### `.encrypt(key, plaintext, nonce, aad?): Uint8Array`
Encrypts plaintext using AES-256-GCM.
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `Uint8Array` | 32-byte (256-bit) AES key |
| `plaintext` | `Uint8Array` | Bytes to encrypt |
| `nonce` | `Uint8Array` | 12-byte IV (use a fresh random nonce per message) |
| `aad` | `Uint8Array \| undefined` | Optional additional authenticated data |
**Returns:** `Uint8Array` (runtime: `Promise<Uint8Array>`) — ciphertext with 16-byte GCM tag appended.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
#### `.decrypt(key, ciphertext, nonce, aad?): Uint8Array`
Decrypts ciphertext and verifies the GCM authentication tag.
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `Uint8Array` | 32-byte AES key (must match encryption key) |
| `ciphertext` | `Uint8Array` | Ciphertext with 16-byte GCM tag appended |
| `nonce` | `Uint8Array` | 12-byte IV used during encryption |
| `aad` | `Uint8Array \| undefined` | Optional AAD (must match encryption AAD exactly) |
**Returns:** `Uint8Array` (runtime: `Promise<Uint8Array>`) — decrypted plaintext.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` on authentication failure or bad key.
## Implementation Notes
`crypto.subtle.importKey` is called on every encrypt/decrypt invocation rather than being cached, because raw `Uint8Array` keys are the idiomatic interface in `AeadEngine` and caching a `CryptoKey` across calls would require a `Map<Uint8Array, CryptoKey>` with careful lifecycle management. The overhead of `importKey` is dominated by the actual AES-GCM operation for typical payload sizes.
The `AeadEngine` interface uses synchronous return types (`Uint8Array`), while `crypto.subtle` is entirely asynchronous. The implementation returns `Promise<Uint8Array>` cast to `Uint8Array` at the type level. Facade functions in `@suaveplan/crypto` use `await` on engine results, so this is transparent to consumers using those facades.
The 12-byte nonce is the NIST-recommended IV length for AES-GCM and provides the best performance with `crypto.subtle`. Longer IVs (96–128 bits) would work but require internal counter splitting which is not done here — callers must supply exactly a 12-byte nonce for maximum compatibility.

View File

@@ -0,0 +1,118 @@
# webcrypto-agree-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides two `AgreeEngine` implementations — `webCryptoX25519Engine` (X25519 Diffie-Hellman) and `webCryptoEcdhP256Engine` (ECDH with P-256) — both backed by the native `crypto.subtle` API. A convenience alias `webCryptoAgreeEngine` resolves to the X25519 engine. These engines implement the `AgreeEngine` interface from `@suaveplan/crypto/engines`, making them drop-in substitutes for the default `@noble/curves`-backed engine in environments where `crypto.subtle` is available.
Key agreement is the foundation of forward-secrecy protocols (TLS, Noise, Signal). Using `crypto.subtle` for ECDH ensures hardware acceleration is used when the runtime supports it, and the key material never leaves the key management subsystem if non-extractable keys are used.
## Features
- `generateKeypair()` — generates an ephemeral X25519 or ECDH-P256 keypair; returns portable raw bytes
- `agree(mySecret, theirPublic)` — derives a 32-byte shared secret via `crypto.subtle.deriveBits`
- Full DH symmetry: `agree(alice, bob.pub) === agree(bob, alice.pub)` for any keypair pair
- All methods are async at runtime despite `AgreeEngine` sync declarations
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` on generation or agreement failure
## Usage: Basic Example
```ts
import { webCryptoX25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-agree-engine";
const alice = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const bob = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const aliceSecret = await (webCryptoX25519Engine.agree(alice.secretKey, bob.publicKey) as unknown as Promise<Uint8Array>);
const bobSecret = await (webCryptoX25519Engine.agree(bob.secretKey, alice.publicKey) as unknown as Promise<Uint8Array>);
// aliceSecret deepEquals bobSecret — both parties derived the same 32 bytes
```
## Usage: Advanced Example
```ts
import { webCryptoX25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-agree-engine";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
// Simple Diffie-Hellman key exchange followed by AES-GCM encryption
async function ephemeralEncrypt(
recipientPublicKey: Uint8Array,
plaintext: Uint8Array
): Promise<{ ephemeralPublic: Uint8Array; ciphertext: Uint8Array }> {
const ephemeral = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const sharedSecret = await (webCryptoX25519Engine.agree(
ephemeral.secretKey,
recipientPublicKey
) as unknown as Promise<Uint8Array>);
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await (webCryptoAeadEngine.encrypt(
sharedSecret,
plaintext,
nonce
) as unknown as Promise<Uint8Array>);
const out = new Uint8Array(12 + ciphertext.length);
out.set(nonce, 0);
out.set(ciphertext, 12);
return { ephemeralPublic: ephemeral.publicKey, ciphertext: out };
}
```
## API Reference
### `webCryptoX25519Engine: AgreeEngine`
X25519 engine via `crypto.subtle` with `{ name: "X25519" }`.
#### `.generateKeypair(): AgreeKeypair`
Returns `Promise<AgreeKeypair>` at runtime. Public key is 32 raw bytes. Secret key is 32-byte scalar.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.
#### `.agree(mySecret, theirPublic): Uint8Array`
Returns `Promise<Uint8Array>` (32 bytes) at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `mySecret` | `Uint8Array` | 32-byte X25519 private scalar |
| `theirPublic` | `Uint8Array` | 32-byte X25519 public key |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `webCryptoEcdhP256Engine: AgreeEngine`
ECDH-P256 engine via `crypto.subtle`. Public key is 65-byte uncompressed point. Secret key is PKCS8 DER. `agree()` returns 32 bytes via `deriveBits(..., 256)`.
---
### `webCryptoAgreeEngine: AgreeEngine`
Alias for `webCryptoX25519Engine`.
## Implementation Notes
X25519 public keys are exported from `crypto.subtle` in SPKI format and the raw 32-byte scalar is extracted from the last 32 bytes of the DER envelope. On import the scalar is re-wrapped with a minimal 48-byte PKCS8 structure. This keeps the external representation simple (just 32 bytes) while satisfying `crypto.subtle`'s requirement for DER-encoded keys.
For ECDH-P256 the key agreement produces a 256-bit shared secret by calling `deriveBits` with `length: 256`. The raw shared secret should be passed through a KDF (e.g., HKDF) before use as an encryption key; this is not done inside the engine so callers retain full control over key derivation.
X25519 requires Node 20+, Bun 1.0+, or Chrome 113+/Firefox 130+/Safari 15.4+. Older environments will receive `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.

View File

@@ -0,0 +1,119 @@
# webcrypto-sign-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides two `SignEngine` implementations — `webCryptoEd25519Engine` (Ed25519) and `webCryptoEcdsaP256Engine` (ECDSA with P-256) — both backed by `crypto.subtle`. The module also exports a convenience alias `webCryptoSignEngine` that resolves to the Ed25519 engine. These engines implement the `SignEngine` interface from `@suaveplan/crypto/engines`, making them drop-in replacements for the default `@noble/curves`-backed engine wherever `crypto.subtle` is available.
Ed25519 provides faster key generation, smaller signatures (64 bytes), and stronger security properties for most use-cases. ECDSA-P256 is provided for interoperability with systems that require NIST curves.
## Features
- `generateKeypair()` — generates an ephemeral Ed25519 or ECDSA-P256 keypair; returns raw public key bytes and raw/PKCS8 private key bytes
- `sign(secretKey, message)` — signs an arbitrary byte array using `crypto.subtle.sign`
- `verify(publicKey, signature, message)` — verifies a signature using `crypto.subtle.verify`
- All methods are async at runtime despite the `SignEngine` interface's sync declarations
- All methods guard with `assertWebCryptoAvailable()` before any `crypto.subtle` calls
- Typed `BrowserCryptoError` thrown on generation failures and operation failures
## Usage: Basic Example
```ts
import { webCryptoEd25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-sign-engine";
const kp = await (webCryptoEd25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const message = new TextEncoder().encode("hello");
const signature = await (webCryptoEd25519Engine.sign(kp.secretKey, message) as unknown as Promise<Uint8Array>);
const valid = await (webCryptoEd25519Engine.verify(kp.publicKey, signature, message) as unknown as Promise<boolean>);
console.log(valid); // true
```
## Usage: Advanced Example
```ts
import {
webCryptoEcdsaP256Engine,
webCryptoEd25519Engine,
} from "@suaveplan/crypto-browser/engines/webcrypto-sign-engine";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// Use P-256 for JWS / JOSE interoperability
async function signJwsPayload(payload: Uint8Array): Promise<{ publicKey: Uint8Array; signature: Uint8Array }> {
try {
const kp = await (webCryptoEcdsaP256Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const signature = await (webCryptoEcdsaP256Engine.sign(kp.secretKey, payload) as unknown as Promise<Uint8Array>);
return { publicKey: kp.publicKey, signature };
} catch (err) {
if (err instanceof BrowserCryptoError) {
// Structured error with err.code = "BROWSER_CRYPTO.KEY.GENERATION_FAILED"
// or "BROWSER_CRYPTO.OPERATION.FAILED"
throw err;
}
throw err;
}
}
```
## API Reference
### `webCryptoEd25519Engine: SignEngine`
Ed25519 engine via `crypto.subtle` with `{ name: "Ed25519" }` algorithm.
#### `.generateKeypair(): SignKeypair`
Returns `Promise<SignKeypair>` at runtime. Public key is 32 raw bytes. Secret key is the 32-byte Ed25519 seed.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.
#### `.sign(secretKey, message): Uint8Array`
Returns `Promise<Uint8Array>` at runtime. Signature is 64 bytes.
| Parameter | Type | Description |
|-----------|------|-------------|
| `secretKey` | `Uint8Array` | 32-byte Ed25519 seed |
| `message` | `Uint8Array` | Arbitrary bytes to sign |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
#### `.verify(publicKey, signature, message): boolean`
Returns `Promise<boolean>` at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `publicKey` | `Uint8Array` | 32-byte Ed25519 public key |
| `signature` | `Uint8Array` | 64-byte signature |
| `message` | `Uint8Array` | Original message bytes |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `webCryptoEcdsaP256Engine: SignEngine`
ECDSA-P256 engine via `crypto.subtle` with `{ name: "ECDSA", namedCurve: "P-256" }`. Same method signatures as above. Public key is 65-byte uncompressed (0x04 prefix). Secret key is PKCS8 DER.
---
### `webCryptoSignEngine: SignEngine`
Alias for `webCryptoEd25519Engine`. Use this when you want the recommended default.
## Implementation Notes
Key export: `crypto.subtle.exportKey("pkcs8", privateKey)` returns the PKCS8 DER-encoded private key. For Ed25519, the 32-byte seed is located at byte offset 16 within the DER envelope; this is extracted to produce the compact secret key stored and transported by callers. On import, the seed is re-wrapped into the minimal 48-byte PKCS8 ASN.1 structure before calling `crypto.subtle.importKey`.
For ECDSA-P256, public keys are exported via `"raw"` format (65-byte uncompressed point), which is directly importable via `"raw"` as well. This avoids SPKI round-trip complexity.
Ed25519 support requires Node 20+, Bun 1.0+, or a modern browser (Chrome 113+, Firefox 129+, Safari 17+). Calling these engines in older environments will produce a `BrowserCryptoError.KEY.GENERATION_FAILED` from the `crypto.subtle.generateKey` rejection.