first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
# `key-generator`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides idempotency key generation and payload fingerprinting utilities for `@suaveplan/idempotency`. Key generation uses `globalThis.crypto.randomUUID()` for random keys and a djb2 hash for deterministic composite keys. Fingerprinting enables mismatch detection: the same idempotency key reused with a different payload fingerprint triggers an `IdempotencyMismatchError`.
## Features
- `generateIdempotencyKey(config?)` — random UUID-based key with optional prefix/separator
- `compositeKey(...parts)` — deterministic djb2 hash of ordered string parts; bounded-length output
- `fingerprintPayload(payload)` — synchronous djb2 fingerprint with stable JSON serialization (key-order independent)
- `fingerprintPayloadAsync(payload)` — SHA-256 fingerprint via `globalThis.crypto.subtle` for cryptographic strength
- Recursive stable `stableStringify` ensuring `{ a: 1, b: 2 }` === `{ b: 2, a: 1 }`
- Approved use of `globalThis.crypto.randomUUID` and `globalThis.crypto.subtle` (see `tests/no-disallowed.test.ts`)
## Usage: Basic Example
```ts
import {
generateIdempotencyKey,
compositeKey,
fingerprintPayload,
} from "@suaveplan/idempotency/key";
// Random key with optional prefix
const key = generateIdempotencyKey({ prefix: "order" });
// e.g. "order:550e8400-e29b-41d4-a716-446655440000"
// Deterministic key from known identifiers
const k = compositeKey("user-42", "order-99");
// djb2 hex of "user-42:order-99"
// Payload fingerprint for mismatch detection
const fp = fingerprintPayload({ amount: 100, currency: "USD" });
```
## Usage: Advanced Example
```ts
import {
compositeKey,
fingerprintPayloadAsync,
generateIdempotencyKey,
} from "@suaveplan/idempotency/key";
// SHA-256 fingerprint for high-value operations
async function guardPayment(userId: string, body: PaymentBody) {
const key = compositeKey("payment", userId, body.orderId);
const fp = await fingerprintPayloadAsync(body);
return guard.execute(key, () => processPayment(body), body);
}
// Prefixed keys for multi-tenant systems
const tenantKey = generateIdempotencyKey({
prefix: `tenant:${tenantId}`,
separator: ":",
});
```
## API Reference
### `generateIdempotencyKey(config?: KeyGeneratorConfig): string`
Generates a random key using `globalThis.crypto.randomUUID()`. If `config.prefix` is set, it is prepended with the separator (default `':'`).
**Params:**
- `config.prefix?: string` — optional prefix string
- `config.separator?: string` — separator between parts (default `':'`)
**Returns:** `string` — a unique key
### `compositeKey(...parts: string[]): string`
Creates a deterministic key from one or more ordered string parts. Internally joins with `':'` and applies djb2 hashing to produce a fixed-length hex string.
**Throws:** `RangeError` when called with zero parts.
### `fingerprintPayload(payload: unknown): string`
Synchronous djb2 fingerprint of any JSON-serializable payload. Object key ordering is normalized before hashing.
**Returns:** `string` — 8-character hex fingerprint.
### `fingerprintPayloadAsync(payload: unknown): Promise<string>`
SHA-256 fingerprint using `globalThis.crypto.subtle.digest`. Requires an async context.
**Returns:** `Promise<string>` — 64-character lower-case hex string.
## Implementation Notes
- djb2 was chosen for synchronous fingerprinting due to its simplicity and zero-dependency nature. It is **not** collision-resistant for adversarial inputs; use `fingerprintPayloadAsync` where cryptographic strength matters.
- `stableStringify` sorts object keys recursively, making `{ b: 2, a: 1 }` and `{ a: 1, b: 2 }` produce identical digests. Arrays preserve order.
- `globalThis.crypto.randomUUID()` is available in Node 15+, Bun, Deno, and all modern browsers without any polyfill.
- `globalThis.crypto.subtle` is available in the same environments. It is **not** available in some older edge runtimes; fall back to `fingerprintPayload` if needed.