Files
skills/docs/foundation/flow/idempotency/src/key/key-generator.md
2026-05-29 08:20:56 +01:00

3.9 KiB

key-generator

(no submodules)

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

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

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.