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,13 @@
|
||||
# `webhook-client`
|
||||
|
||||
Webhook delivery client with retry logic and HMAC verification.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Packages in `webhook-client`
|
||||
|
||||
1 package.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/webhook-client`](./webhook-client/README.md) | Webhook delivery client with retry logic, HMAC signature generation/verification, payload transformation, and delivery tracking. Runtime-agnostic (injectable f… |
|
||||
<!-- end:auto-docs -->
|
||||
@@ -0,0 +1 @@
|
||||
Webhook delivery client with retry logic and HMAC verification.
|
||||
@@ -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` |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user