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,104 @@
# @suaveplan/webhook-client - Webhook delivery client with retry logic, HMAC signature generation/verification, payload transformation, and delivery tracking
## Summary
Webhook delivery client with retry logic, HMAC signature generation/verification, payload transformation, and delivery tracking. Runtime-agnostic (injectable fetch).
<!-- begin:auto-docs -->
## Documentation tree
- [client](./src/core/client/webhook-client.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [webhook-client](./src/core/client/webhook-client.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [webhook-delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [webhook-client-error](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [payload-builder](./src/utils/payload-builder.md) — Provides utilities for constructing and serializing `WebhookPayload` objects before delivery. The two functions in this…
- [signature](./src/utils/signature.md) — Provides HMAC signature generation and verification for webhook payloads using the Web Crypto API (`globalThis.crypto.s…
<!-- end:auto-docs --> - [webhook-client](./src/core/client/webhook-client.md) — <!-- begin:auto-docs --> _(no submodules)_- [delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_- [webhook-delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [webhook-client-error](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [payload-builder](./src/utils/payload-builder.md) — Provides utilities for constructing and serializing `WebhookPayload` objects before delivery. The two functions in this…
- [signature](./src/utils/signature.md) — Provides HMAC signature generation and verification for webhook payloads using the Web Crypto API (`globalThis.crypto.s…
- [webhook-client](./src/core/client/webhook-client.md) — <!-- begin:auto-docs --> _(no submodules)_- [delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_- [webhook-delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [webhook-client-error](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [payload-builder](./src/utils/payload-builder.md) — Provides utilities for constructing and serializing `WebhookPayload` objects before delivery. The two functions in this…
- [signature](./src/utils/signature.md) — Provides HMAC signature generation and verification for webhook payloads using the Web Crypto API (`globalThis.crypto.s…
- [webhook-client](./src/core/client/webhook-client.md) — <!-- begin:auto-docs --> _(no submodules)_- [delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_- [webhook-delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [webhook-client-error](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [payload-builder](./src/utils/payload-builder.md) — Provides utilities for constructing and serializing `WebhookPayload` objects before delivery. The two functions in this…
- [signature](./src/utils/signature.md) — Provides HMAC signature generation and verification for webhook payloads using the Web Crypto API (`globalThis.crypto.s…
- [webhook-client](./src/core/client/webhook-client.md) — <!-- begin:auto-docs --> _(no submodules)_- [delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_ - [webhook-delivery](./src/core/delivery/webhook-delivery.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [webhook-client-error](./src/errors/webhook-client-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [payload-builder](./src/utils/payload-builder.md) — Provides utilities for constructing and serializing `WebhookPayload` objects before delivery. The two functions in this…
- [signature](./src/utils/signature.md) — Provides HMAC signature generation and verification for webhook payloads using the Web Crypto API (`globalThis.crypto.s…
## Installation
```bash
bun add @suaveplan/webhook-client
```
## Quick Start
```ts
import { createWebhookClient } from '@suaveplan/webhook-client';
const client = createWebhookClient({
url: 'https://partner.example.com/webhooks',
secret: 'shared-secret',
maxRetries: 3,
retryDelay: 1000,
});
const result = await client.send('order.created', { orderId: 42 });
console.log(result.status); // 'delivered'
```
## Modules
| Module | Path | Description |
|--------|------|-------------|
| Client | [src/core/client/webhook-client.md](src/core/client/webhook-client.md) | `WebhookClient` class and `createWebhookClient` factory |
| Delivery | [src/core/delivery/webhook-delivery.md](src/core/delivery/webhook-delivery.md) | Core HTTP delivery engine with retry logic |
| Signature | [src/utils/signature.md](src/utils/signature.md) | HMAC signature generation and verification |
| Payload Builder | [src/utils/payload-builder.md](src/utils/payload-builder.md) | Payload construction and deterministic serialization |
| Errors | [src/errors/webhook-client-error.md](src/errors/webhook-client-error.md) | Error codes and `WebhookClientError` class |
## Subpath Exports
```ts
import { createWebhookClient } from '@suaveplan/webhook-client';
import { deliver } from '@suaveplan/webhook-client/delivery';
import { generateSignature, verifySignature } from '@suaveplan/webhook-client/signature';
import { WEBHOOK_CLIENT_CODES, WebhookClientError } from '@suaveplan/webhook-client/errors';
```
## Configuration
```ts
interface WebhookConfig {
url: string;
secret?: string; // HMAC secret
algorithm?: 'sha256' | 'sha512'; // default: 'sha256'
headers?: Record<string, string>; // additional headers
timeout?: number; // ms, default: 30000
maxRetries?: number; // default: 3
retryDelay?: number; // base delay ms, default: 1000
}
```
## Retry Behaviour
Delivery uses exponential backoff with full jitter via `@suaveplan/retry`:
- **2xx** → success, no retry
- **429 / 5xx** → transient, retried up to `maxRetries`
- **4xx (except 429)** → terminal, not retried
## HMAC Signatures
When `secret` is configured, every delivery attaches:
- `X-Hub-Signature-256: sha256=<hex>` (default)
- `X-Hub-Signature-512: sha512=<hex>` (when `algorithm: 'sha512'`)
Verification uses Web Crypto API constant-time comparison.
## OTel Metrics
| Metric | Type | Labels |
|--------|------|--------|
| `webhook_client.delivery.total` | Counter | `event`, `status` |
| `webhook_client.delivery.duration_ms` | Histogram | `event`, `status` |

View File

@@ -0,0 +1,115 @@
# webhook-client
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`WebhookClient` is the primary consumer-facing class for `@suaveplan/webhook-client`. It composes payload construction, HMAC signing, retry-backed HTTP delivery, and incoming signature verification behind a clean stateful API. The immutable `withHeaders` builder allows middleware layers to augment headers without mutating shared configuration, making the client safe to share across request handlers.
## Features
- `send(event, data)` — builds a payload and delivers it in one call
- `sendPayload(payload)` — delivers a pre-built payload (useful with idempotency guards)
- `verify(payload, signature)` — verifies an incoming webhook signature using this client's secret
- `withHeaders(headers)` — returns a new client with merged headers (immutable builder pattern)
- `createWebhookClient(config, options?)` — factory function (preferred over `new`)
- OTel metrics emitted by the underlying `deliver` function automatically
- Injectable `fetch` via `DeliveryOptions` for test isolation
## Usage: Basic Example
```ts
import { createWebhookClient } from '@suaveplan/webhook-client';
const client = createWebhookClient({
url: 'https://partner.example.com/webhooks',
secret: 'shared-secret',
});
const result = await client.send('order.created', { orderId: 42 });
console.log(result.status); // 'delivered'
```
## Usage: Advanced Example
```ts
import { createWebhookClient } from '@suaveplan/webhook-client';
// Base client shared across the service
const base = createWebhookClient({
url: 'https://partner.example.com/webhooks',
secret: process.env.WEBHOOK_SECRET,
algorithm: 'sha256',
timeout: 10000,
maxRetries: 5,
retryDelay: 500,
});
// Per-request client with trace headers injected from incoming request
const perRequest = base.withHeaders({
'X-Trace-Id': ctx.traceId,
'X-Request-Id': ctx.requestId,
});
const result = await perRequest.send('payment.processed', {
invoiceId: ctx.invoiceId,
amount: ctx.amount,
});
// Verify incoming webhook from same partner
const incomingBody = await request.text();
const incomingSig = request.headers.get('X-Hub-Signature-256') ?? '';
const valid = await base.verify(incomingBody, incomingSig);
```
## API Reference
### `createWebhookClient(config, options?)`
```ts
function createWebhookClient(
config: WebhookConfig,
options?: DeliveryOptions
): WebhookClient
```
Factory function. Prefer this over `new WebhookClient(...)`.
### `WebhookClient`
#### `send(event, data)`
```ts
async send(event: string, data: unknown): Promise<DeliveryResult>
```
Constructs a `WebhookPayload` via `buildPayload` and delivers it. Returns a `DeliveryResult` with the full attempt history.
#### `sendPayload(payload)`
```ts
async sendPayload(payload: WebhookPayload): Promise<DeliveryResult>
```
Delivers a pre-built payload. Useful when you need to inject a specific `id` from an idempotency guard.
#### `verify(payload, signature)`
```ts
async verify(payload: string, signature: string): Promise<boolean>
```
Verifies an incoming webhook signature against the client's configured `secret` and `algorithm`. Returns `false` if no secret is configured.
#### `withHeaders(headers)`
```ts
withHeaders(headers: Record<string, string>): WebhookClient
```
Returns a **new** `WebhookClient` with `headers` merged into the existing config. The original instance is unchanged.
## Implementation Notes
`WebhookClient` uses `protected` rather than `private` for `config` and `options` to allow subclassing for specialized clients (e.g. a `SignedWebhookClient` that enforces secret presence). The `withHeaders` method creates a shallow merge — later-passed headers override earlier ones — matching standard HTTP header override semantics. The `verify` method returns `false` when no secret is configured rather than throwing, since receiving an unsigned request in that context is ambiguous and callers should handle it explicitly.

View File

@@ -0,0 +1,98 @@
# webhook-delivery
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the core HTTP delivery engine for `@suaveplan/webhook-client`. It handles payload serialization, HMAC signature attachment, exponential-backoff retry (via `@suaveplan/retry`), per-attempt tracking, and OTel metric emission. The fetch implementation is injectable, making the module fully testable without network access or mocking frameworks.
## Features
- `deliver` — async function that POSTs a `WebhookPayload` to a configured URL with retry logic
- Exponential backoff with full jitter via `@suaveplan/retry`'s `createExponentialBackoff`
- HMAC signature attached as `X-Hub-Signature-256` or `X-Hub-Signature-512` when a secret is configured
- Standard webhook headers: `X-Webhook-Id`, `X-Webhook-Event`, `X-Webhook-Timestamp`
- Per-attempt callback (`onAttempt`) for observability hooks
- Per-request AbortSignal timeout
- Retry logic distinguishes retryable (429, 5xx, network errors) from terminal (4xx except 429) failures
- OTel counter `webhook_client.delivery.total{event, status}` and histogram `webhook_client.delivery.duration_ms{event, status}`
- Injectable `fetch` for test isolation without mocks
## Usage: Basic Example
```ts
import { deliver } from '@suaveplan/webhook-client/delivery';
import { buildPayload } from '@suaveplan/webhook-client';
const payload = buildPayload('order.created', { orderId: 42 });
const result = await deliver(
{ url: 'https://partner.example.com/webhooks', secret: 'my-secret' },
payload
);
console.log(result.status); // 'delivered'
console.log(result.attempts.length); // 1 (no retries needed)
```
## Usage: Advanced Example
```ts
import { deliver } from '@suaveplan/webhook-client/delivery';
import { buildPayload } from '@suaveplan/webhook-client';
const payload = buildPayload('payment.failed', { invoiceId: 'inv-001' });
const result = await deliver(
{
url: 'https://partner.example.com/webhooks',
secret: 'shared-secret',
algorithm: 'sha512',
timeout: 5000,
maxRetries: 5,
retryDelay: 500,
headers: { 'X-Source': 'billing-service' },
},
payload,
{
onAttempt: (attempt) => {
console.log(`Attempt ${attempt.attemptNumber}: ${attempt.success ? 'ok' : attempt.error}`);
},
}
);
```
## API Reference
### `DeliveryOptions`
```ts
interface DeliveryOptions {
fetch?: (url: string, init: RequestInit) => Promise<Response>;
onAttempt?: (attempt: DeliveryAttempt) => void;
}
```
- `fetch` — injectable fetch implementation; defaults to `globalThis.fetch`
- `onAttempt` — callback fired after each attempt with the attempt record
### `deliver(config, payload, options?)`
```ts
async function deliver(
config: WebhookConfig,
payload: WebhookPayload,
options?: DeliveryOptions
): Promise<DeliveryResult>
```
- `config` — endpoint configuration including URL, secret, timeouts, retry settings
- `payload` — pre-built `WebhookPayload` (use `buildPayload` to construct one)
- `options` — optional injectable fetch and attempt callback
- Returns a `DeliveryResult` with full attempt history and final status
- Throws `WebhookClientError` (code `DELIVERY_FAILED`) if all retries are exhausted
## Implementation Notes
The signature is computed once before the retry loop because HMAC output is deterministic for the same payload and secret — re-computing per attempt would be wasteful. The `shouldRetry` predicate in the retry config marks non-retryable 4xx errors as terminal by attaching a `_terminal` flag to the thrown error; this avoids wasting retry budget on errors that will never resolve. OTel metrics are lazily initialised on the first call to keep the module side-effect-free at import time.

View File

@@ -0,0 +1,92 @@
# webhook-client-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable error code registry and typed error class for all `@suaveplan/webhook-client` failures. By extending `SuaveplanError` with a known set of codes, consumers can discriminate webhook errors from other `SuaveplanError` instances using both `instanceof` checks and the stable `code` string without coupling themselves to error message text.
## Features
- `WEBHOOK_CLIENT_CODES` — stable code registry (`defineCodes`) covering delivery, signature, config, and timeout failure modes
- `WebhookClientError` — typed error class that carries a stable `code`, optional `cause`, and integrates with the project error chain traversal utilities
- `WebhookClientCode` — union type of all registered code values for exhaustive switch statements
## Usage: Basic Example
```ts
import { WEBHOOK_CLIENT_CODES, WebhookClientError } from '@suaveplan/webhook-client/errors';
throw new WebhookClientError(
'Delivery failed after 3 retries',
WEBHOOK_CLIENT_CODES.DELIVERY_FAILED
);
```
## Usage: Advanced Example
```ts
import { WEBHOOK_CLIENT_CODES, WebhookClientError } from '@suaveplan/webhook-client/errors';
import { isSuaveplanError } from '@suaveplan/error';
async function deliver(url: string): Promise<void> {
try {
await fetch(url);
} catch (err) {
throw new WebhookClientError(
'Delivery failed',
WEBHOOK_CLIENT_CODES.DELIVERY_FAILED,
{ cause: err instanceof Error ? err : undefined }
);
}
}
// Discriminate at the call site
try {
await deliver('https://example.com/webhook');
} catch (err) {
if (isSuaveplanError(err) && err.code === WEBHOOK_CLIENT_CODES.DELIVERY_FAILED) {
console.error('Webhook failed:', err.message);
}
}
```
## API Reference
### `WEBHOOK_CLIENT_CODES`
```ts
const WEBHOOK_CLIENT_CODES: {
readonly DELIVERY_FAILED: string;
readonly SIGNATURE_FAILED: string;
readonly INVALID_CONFIG: string;
readonly TIMEOUT: string;
}
```
Stable string codes registered via `defineCodes('WEBHOOK_CLIENT', ...)`. Each key maps to a namespaced code like `"WEBHOOK_CLIENT.DELIVERY_FAILED"`.
### `WebhookClientCode`
```ts
type WebhookClientCode = (typeof WEBHOOK_CLIENT_CODES)[keyof typeof WEBHOOK_CLIENT_CODES];
```
Union type of all registered code values.
### `WebhookClientError`
```ts
class WebhookClientError extends SuaveplanError {
constructor(message: string, code: string, options?: { cause?: Error }): void;
}
```
- `message` — human-readable error description
- `code` — one of `WEBHOOK_CLIENT_CODES` values
- `options.cause` — optional upstream error for cause-chain traversal
## Implementation Notes
The constructor signature uses `string` rather than the narrower `WebhookClientCode` union to avoid forcing callers to import the union type just for type-checking purposes. The canonical usage pattern is to always pass a value from `WEBHOOK_CLIENT_CODES`. The `name` is explicitly set to `"WebhookClientError"` so that serialized stack traces are immediately identifiable without class introspection.

View File

@@ -0,0 +1,164 @@
# webhook-client.types
## Purpose
Defines the TypeScript type vocabulary for the `@suaveplan/webhook-client` package. All public interfaces, union types, and type aliases are gathered here so that every other module in the package imports from a single canonical location, preventing circular dependencies and keeping type definitions easy to discover and evolve.
These types are also re-exported from the package root so consumer code can import them directly from `@suaveplan/webhook-client` without reaching into internal paths.
## Features
- `WebhookSignatureAlgorithm` — union of supported HMAC hash algorithms (`"sha256"` | `"sha512"`)
- `DeliveryStatus` — lifecycle discriminant covering the four states a delivery can be in
- `WebhookConfig` — full endpoint configuration including URL, secret, algorithm, headers, timeout, and retry settings
- `WebhookPayload` — canonical envelope sent to subscriber endpoints with stable key order
- `DeliveryAttempt` — per-attempt record capturing HTTP status, duration, success flag, and optional error message
- `DeliveryResult` — aggregated outcome of an entire delivery run with all attempt history and final status code
## Usage: Basic Example
```typescript
import type {
WebhookConfig,
WebhookPayload,
DeliveryResult,
} from "@suaveplan/webhook-client";
const config: WebhookConfig = {
url: "https://partner.example.com/hooks",
secret: "shared-secret",
algorithm: "sha256",
maxRetries: 3,
retryDelay: 500,
timeout: 10000,
};
const payload: WebhookPayload = {
id: "d290f1ee-6c54-4b01-90e6-d701748f0851",
event: "order.created",
data: { orderId: 42, total: 99.99 },
timestamp: new Date().toISOString(),
};
```
## Usage: Advanced Example
```typescript
import type {
DeliveryAttempt,
DeliveryResult,
DeliveryStatus,
} from "@suaveplan/webhook-client";
function summarise(result: DeliveryResult): string {
const status: DeliveryStatus = result.status;
const attempts: DeliveryAttempt[] = result.attempts;
const lines = attempts.map(
(a) =>
` attempt ${a.attemptNumber}: ${a.success ? "ok" : `fail(${a.statusCode ?? "no-response"})`} in ${a.duration}ms`
);
return [`delivery ${result.id} — ${status}`, ...lines].join("\n");
}
// Narrow on DeliveryStatus to gate downstream processing
function isTerminal(status: DeliveryStatus): boolean {
return status === "delivered" || status === "failed";
}
```
## API Reference
### `WebhookSignatureAlgorithm`
```typescript
type WebhookSignatureAlgorithm = "sha256" | "sha512";
```
Union of HMAC algorithms supported by `generateSignature` and `verifySignature`. Drives which Web Crypto hash (`SHA-256` or `SHA-512`) is used and which HTTP header name (`X-Hub-Signature-256` or `X-Hub-Signature-512`) is set on delivery.
---
### `DeliveryStatus`
```typescript
type DeliveryStatus = "pending" | "delivered" | "failed" | "retrying";
```
Lifecycle state of a webhook delivery operation.
| Value | Description |
|---|---|
| `"pending"` | First attempt in progress — not yet resolved |
| `"retrying"` | A prior attempt failed; a subsequent attempt is in progress |
| `"delivered"` | A 2xx response was received; delivery is complete |
| `"failed"` | All attempts exhausted or a non-retryable status was received |
---
### `WebhookConfig`
Full configuration object for a webhook endpoint.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `url` | `string` | Yes | — | Target URL to POST webhook payloads to |
| `secret` | `string` | No | — | Shared HMAC secret; omit to skip signing |
| `algorithm` | `WebhookSignatureAlgorithm` | No | `"sha256"` | Hash algorithm for HMAC |
| `headers` | `Record<string, string>` | No | `{}` | Additional HTTP headers on every delivery |
| `timeout` | `number` | No | `30000` | Per-request timeout in milliseconds |
| `maxRetries` | `number` | No | `3` | Total delivery attempts (including first) |
| `retryDelay` | `number` | No | `1000` | Base retry delay (ms) for exponential backoff |
---
### `WebhookPayload`
The canonical envelope sent to a subscriber endpoint.
| Field | Type | Description |
|---|---|---|
| `id` | `string` | Unique delivery ID (UUID v4 or timestamp-based fallback) |
| `event` | `string` | Event type discriminator (e.g. `"order.created"`) |
| `data` | `unknown` | Arbitrary event data |
| `timestamp` | `string` | ISO 8601 UTC creation time |
---
### `DeliveryAttempt`
Record of a single HTTP POST attempt.
| Field | Type | Description |
|---|---|---|
| `attemptNumber` | `number` | 1-based attempt index |
| `timestamp` | `string` | ISO 8601 time the attempt started |
| `statusCode` | `number?` | HTTP status if a response was received |
| `success` | `boolean` | Whether the response was 2xx |
| `error` | `string?` | Human-readable error message on failure |
| `duration` | `number` | Round-trip time in milliseconds |
---
### `DeliveryResult`
Aggregated outcome of an entire webhook delivery run.
| Field | Type | Description |
|---|---|---|
| `id` | `string` | Delivery ID matching `WebhookPayload.id` |
| `status` | `DeliveryStatus` | Final status after all attempts |
| `attempts` | `DeliveryAttempt[]` | Ordered attempt history |
| `payload` | `WebhookPayload` | The original payload delivered |
| `finalStatusCode` | `number?` | Final HTTP status code if a response was received |
## Implementation Notes
All types are interface or type-alias declarations only — no runtime code is emitted. This makes the module fully tree-shakable and zero-cost to import in both Node.js and browser environments.
The `data` field on `WebhookPayload` is typed as `unknown` rather than `Record<string, unknown>` to accommodate primitive payloads (numbers, strings, arrays) without forcing callers to cast. Downstream consumers who need a narrower type should use a Zod schema or type guard at the boundary.
`DeliveryAttempt.statusCode` and `DeliveryAttempt.error` are optional to handle the case where `fetch` itself throws before an HTTP response is received (e.g. DNS failure, timeout, network error). In those situations `statusCode` is absent and `error` carries the exception message.
`finalStatusCode` on `DeliveryResult` mirrors `DeliveryAttempt.statusCode` of the last attempt, provided for ergonomic access without array traversal.

View File

@@ -0,0 +1,75 @@
# payload-builder
## Purpose
Provides utilities for constructing and serializing `WebhookPayload` objects before delivery. The two functions in this module are deliberately simple but important for correctness: `buildPayload` guarantees every payload gets a unique ID and an accurate timestamp, while `serializePayload` enforces consistent key ordering so that HMAC signatures computed independently on sender and receiver sides always produce identical digests.
## Features
- `buildPayload` — creates a complete `WebhookPayload` with auto-generated UUID (via `crypto.randomUUID()` with a `Date.now()` fallback) and ISO 8601 timestamp
- `serializePayload` — deterministic JSON serialization with explicit key ordering: `id`, `event`, `data`, `timestamp`
- Zero external dependencies — uses only built-in `JSON.stringify` and the Web Crypto API
## Usage: Basic Example
```ts
import { buildPayload, serializePayload } from '@suaveplan/webhook-client';
const payload = buildPayload('order.created', { orderId: 42, total: 99.99 });
// { id: 'f7e2...', event: 'order.created', data: { orderId: 42, total: 99.99 }, timestamp: '2026-04-19T...' }
const body = serializePayload(payload);
// '{"id":"f7e2...","event":"order.created","data":{"orderId":42,"total":99.99},"timestamp":"..."}'
```
## Usage: Advanced Example
```ts
import { buildPayload, serializePayload } from '@suaveplan/webhook-client';
import { generateSignature } from '@suaveplan/webhook-client/signature';
// Inject a known ID for idempotency key correlation
const payload = buildPayload(
'payment.processed',
{ invoiceId: 'inv-001', amount: 250.00 },
'explicit-delivery-id-from-idempotency-guard'
);
const body = serializePayload(payload);
const signature = await generateSignature(body, process.env.WEBHOOK_SECRET!, 'sha256');
await fetch('https://partner.example.com/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature-256': signature,
},
body,
});
```
## API Reference
### `buildPayload(event, data, id?)`
```ts
function buildPayload(event: string, data: unknown, id?: string): WebhookPayload
```
- `event` — event type string, e.g. `"order.created"`
- `data` — arbitrary payload data (any JSON-serializable value)
- `id` — optional explicit delivery ID; defaults to `crypto.randomUUID()` or a timestamp+random fallback
- Returns a fully formed `WebhookPayload` with `id`, `event`, `data`, and `timestamp`
### `serializePayload(payload)`
```ts
function serializePayload(payload: WebhookPayload): string
```
- `payload` — a `WebhookPayload` object
- Returns a JSON string with keys in the order: `id`, `event`, `data`, `timestamp`
## Implementation Notes
Key ordering in `serializePayload` is enforced by constructing an intermediate object literal with explicit property order before calling `JSON.stringify`. This is more reliable than relying on insertion-order preservation in dynamic objects, especially when payloads are reconstructed from deserialized sources. The `generateId()` fallback uses `Date.now()` hex plus 32-bit random hex, producing IDs that are time-sortable and reasonably unique for delivery tracking in environments where `crypto.randomUUID` is unavailable.

View File

@@ -0,0 +1,107 @@
# signature
## Purpose
Provides HMAC signature generation and verification for webhook payloads using the Web Crypto API (`globalThis.crypto.subtle`). The implementation is runtime-agnostic and works in Node.js 18+, Bun, Deno, and modern browsers. It follows the GitHub webhook signature format (`sha256=<hex>`) which is the de-facto standard for HMAC-authenticated webhooks.
## Features
- `generateSignature` — produces a `"sha256=<hex>"` or `"sha512=<hex>"` signature using HMAC-SHA256 / HMAC-SHA512
- `verifySignature` — constant-time verification via `crypto.subtle.verify` to prevent timing attacks
- `parseSignatureHeader` — parses `"<algorithm>=<value>"` signature headers into structured form, returns `null` for invalid input
- Zero external dependencies — uses only `globalThis.crypto.subtle`
## Usage: Basic Example
```ts
import { generateSignature, verifySignature } from '@suaveplan/webhook-client/signature';
const body = JSON.stringify({ event: 'order.created', data: { id: 1 } });
const secret = 'my-secret';
const sig = await generateSignature(body, secret, 'sha256');
// => "sha256=a3f4b2c1..."
const valid = await verifySignature(body, sig, secret, 'sha256');
// => true
```
## Usage: Advanced Example
```ts
import {
generateSignature,
verifySignature,
parseSignatureHeader,
} from '@suaveplan/webhook-client/signature';
// Incoming request from a webhook provider
const incomingBody = await request.text();
const sigHeader = request.headers.get('X-Hub-Signature-256') ?? '';
const parsed = parseSignatureHeader(sigHeader);
if (!parsed) {
throw new Error('Missing or malformed signature header');
}
const isValid = await verifySignature(
incomingBody,
sigHeader,
process.env.WEBHOOK_SECRET!,
'sha256'
);
if (!isValid) {
return new Response('Unauthorized', { status: 401 });
}
```
## API Reference
### `generateSignature(payload, secret, algorithm)`
```ts
async function generateSignature(
payload: string,
secret: string,
algorithm: WebhookSignatureAlgorithm
): Promise<string>
```
- `payload` — the raw serialized payload string to sign
- `secret` — the shared HMAC secret
- `algorithm` — `'sha256'` or `'sha512'`
- Returns `"sha256=<hex>"` or `"sha512=<hex>"`
- Throws `WebhookClientError` (code `SIGNATURE_FAILED`) if the Web Crypto API fails
### `verifySignature(payload, signature, secret, algorithm)`
```ts
async function verifySignature(
payload: string,
signature: string,
secret: string,
algorithm: WebhookSignatureAlgorithm
): Promise<boolean>
```
- `payload` — the raw payload string
- `signature` — the full signature header value (e.g. `"sha256=abc..."`)
- `secret` — shared secret
- `algorithm` — expected algorithm; returns `false` if algorithm in header differs
- Returns `true` if valid, `false` otherwise (never throws on invalid input)
### `parseSignatureHeader(header)`
```ts
function parseSignatureHeader(
header: string
): { algorithm: string; value: string } | null
```
- `header` — raw header value like `"sha256=abc123"`
- Returns `{ algorithm: 'sha256', value: 'abc123' }` or `null` if malformed
## Implementation Notes
The verification path uses `crypto.subtle.verify` rather than a manual HMAC + constant-time string compare to get OS-level timing attack resistance. The `parseSignatureHeader` function is purely synchronous and safe to call in request validation middleware before any async work. All functions gracefully return `false` or `null` on invalid input rather than throwing, except `generateSignature` which throws since a generation failure is always an unexpected error worth surfacing.