@aimearn/webhook-sdk 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +26 -8
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @aimearn/webhook-sdk changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0 — brand-provided commission (required)
|
|
4
|
+
|
|
5
|
+
- **Breaking:** `CompletedItem.commissionCents` and
|
|
6
|
+
`CompletedData.totalCommissionCents` are now **required**. A payload
|
|
7
|
+
missing either is rejected by the platform with `parse_error` (HTTP 400);
|
|
8
|
+
TypeScript flags the missing fields at compile time.
|
|
9
|
+
- The platform no longer derives commission from `subtotalCents` — the
|
|
10
|
+
brand-provided `commissionCents` is the sole commission source, split
|
|
11
|
+
across the affiliate chain using the per-`ProductCountry` percentages as
|
|
12
|
+
ratios. `subtotalCents` is retained for forensic/reporting only.
|
|
13
|
+
- `Σ item.commissionCents !== totalCommissionCents` → the order is still
|
|
14
|
+
accepted, with a `commission_mismatch` warning in `warnings`.
|
|
15
|
+
|
|
16
|
+
## 0.2.0 — brand-provided commission (optional)
|
|
17
|
+
|
|
18
|
+
- Added optional `CompletedItem.commissionCents` and
|
|
19
|
+
`CompletedData.totalCommissionCents`. When present, the platform used the
|
|
20
|
+
brand-provided commission (split via the `ProductCountry` percentages as
|
|
21
|
+
ratios) instead of deriving it from `subtotalCents`; omitting them fell
|
|
22
|
+
back to the percentage-of-subtotal calculation. (Superseded by 1.0.0,
|
|
23
|
+
which makes both fields required.)
|
|
24
|
+
|
|
3
25
|
## 0.1.0 — initial release
|
|
4
26
|
|
|
5
27
|
- `AimearnClient` with `orderCompleted` / `orderShipped` /
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Brand SDK for the Aim Earn webhook ingest API.
|
|
4
4
|
|
|
5
5
|
Wraps:
|
|
6
|
+
|
|
6
7
|
- HMAC-SHA256 request signing + response-signature verification
|
|
7
8
|
- Exponential-backoff retries on 5xx / 429 / network errors
|
|
8
9
|
- ULID-shaped `eventId` auto-generation for idempotent retries
|
|
@@ -32,9 +33,15 @@ const result = await client.orderCompleted({
|
|
|
32
33
|
orderId: "ord_12345",
|
|
33
34
|
data: {
|
|
34
35
|
items: [
|
|
35
|
-
{
|
|
36
|
+
{
|
|
37
|
+
productId: "shopwave-sku-789",
|
|
38
|
+
quantity: 1,
|
|
39
|
+
unitPriceCents: 19900,
|
|
40
|
+
commissionCents: 1990, // required — brand-computed, post-discount
|
|
41
|
+
},
|
|
36
42
|
],
|
|
37
43
|
subtotalCents: 19900,
|
|
44
|
+
totalCommissionCents: 1990, // required — must equal Σ item.commissionCents
|
|
38
45
|
currency: "SGD",
|
|
39
46
|
country: "SG",
|
|
40
47
|
referralCode: "XX0025",
|
|
@@ -46,6 +53,17 @@ const result = await client.orderCompleted({
|
|
|
46
53
|
|
|
47
54
|
Methods: `orderCompleted`, `orderShipped`, `orderRefunded`, `orderCancelled`.
|
|
48
55
|
|
|
56
|
+
### Commission (required as of 1.0.0)
|
|
57
|
+
|
|
58
|
+
`commissionCents` (per item) and `totalCommissionCents` (order total) are
|
|
59
|
+
**required**. Compute them on your side — apply any discounts/promotions
|
|
60
|
+
first — because the platform treats `commissionCents` as authoritative and
|
|
61
|
+
splits it across the affiliate chain using your configured `ProductCountry`
|
|
62
|
+
percentages as ratios. It never derives commission from `subtotalCents`. A
|
|
63
|
+
payload missing either field is rejected with `parse_error` (HTTP 400); a
|
|
64
|
+
`Σ item.commissionCents !== totalCommissionCents` mismatch is accepted with a
|
|
65
|
+
`commission_mismatch` warning.
|
|
66
|
+
|
|
49
67
|
## Browser usage (affiliate-link capture)
|
|
50
68
|
|
|
51
69
|
```ts
|
|
@@ -55,7 +73,7 @@ captureReferralFromUrl();
|
|
|
55
73
|
|
|
56
74
|
// At checkout
|
|
57
75
|
import { getStoredReferralCode } from "@aimearn/webhook-sdk/browser";
|
|
58
|
-
const referralCode = getStoredReferralCode();
|
|
76
|
+
const referralCode = getStoredReferralCode(); // "XX0025" or null
|
|
59
77
|
```
|
|
60
78
|
|
|
61
79
|
ESM-only, ~1KB gzipped, no external deps.
|
|
@@ -83,12 +101,12 @@ npm run build
|
|
|
83
101
|
|
|
84
102
|
Produces:
|
|
85
103
|
|
|
86
|
-
| Path
|
|
87
|
-
|
|
88
|
-
| `dist/index.js`
|
|
89
|
-
| `dist/index.cjs`
|
|
90
|
-
| `dist/index.d.ts`
|
|
91
|
-
| `dist/browser/index.js`
|
|
104
|
+
| Path | Use |
|
|
105
|
+
| ------------------------- | ------------------- |
|
|
106
|
+
| `dist/index.js` | Server entry, ESM |
|
|
107
|
+
| `dist/index.cjs` | Server entry, CJS |
|
|
108
|
+
| `dist/index.d.ts` | Server entry types |
|
|
109
|
+
| `dist/browser/index.js` | Browser entry, ESM |
|
|
92
110
|
| `dist/browser/index.d.ts` | Browser entry types |
|
|
93
111
|
|
|
94
112
|
## Engines
|
package/dist/index.cjs
CHANGED
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/eventId.ts","../src/signing.ts","../src/errors.ts","../src/retry.ts","../src/client.ts"],"sourcesContent":["// @aimearn/webhook-sdk public surface (server entry).\n//\n// Spec: docs/superpowers/specs/2026-04-29-webhook-ingest-design.md\n// Docs: /docs/sdk in the host app\n\nexport const SDK_VERSION = \"0.1.0\";\n\nexport { AimearnClient } from \"./client\";\nexport type { AimearnClientConfig } from \"./client\";\n\nexport { generateEventId } from \"./eventId\";\nexport {\n signRequest,\n verifyResponseSignature,\n parseSignatureHeader,\n REPLAY_WINDOW_SECONDS,\n} from \"./signing\";\n\nexport {\n AimearnError,\n AimearnAuthError,\n AimearnValidationError,\n AimearnNotFoundError,\n AimearnForbiddenError,\n AimearnConflictError,\n AimearnPlatformError,\n errorFromResponse,\n} from \"./errors\";\nexport type { AimearnErrorCode } from \"./errors\";\n\nexport {\n isRetriable,\n backoffDelayMs,\n resolveRetryConfig,\n} from \"./retry\";\nexport type { RetryConfig } from \"./retry\";\n\nexport type {\n WireEventType,\n CompletedItem,\n CustomerInfo,\n CompletedData,\n ShippedData,\n RefundedData,\n CancelledData,\n Envelope,\n OrderCompletedInput,\n OrderShippedInput,\n OrderRefundedInput,\n OrderCancelledInput,\n AcceptedResponse,\n} from \"./types\";\n","// Stripe-style ULID-shaped idempotency key, formatted as\n// `evt_<26-char-base32-crockford>`. Same external shape as our backend\n// uses, but generated client-side so brands can collapse retries from\n// the same call site naturally — re-issuing the same `eventId` against\n// the platform is idempotent (the ingest Lambda's\n// `attribute_not_exists(orderId)` guard catches duplicates).\n//\n// Deliberately not pulling in the `ulid` npm package — keeping the\n// SDK dependency-free at runtime keeps the install size small and\n// avoids supply-chain surface. The implementation below is a faithful\n// 48-bit-time + 80-bit-randomness ULID with Crockford base32, matching\n// the spec at https://github.com/ulid/spec.\n\nimport { randomBytes } from \"node:crypto\";\n\nconst ENCODING = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\"; // Crockford base32\nconst ENCODING_LEN = ENCODING.length;\nconst TIME_LEN = 10;\nconst RANDOM_LEN = 16;\n\nfunction encodeTime(now: number, len: number): string {\n let out = \"\";\n let n = now;\n for (let i = len - 1; i >= 0; i--) {\n const mod = n % ENCODING_LEN;\n out = ENCODING[mod] + out;\n n = (n - mod) / ENCODING_LEN;\n }\n return out;\n}\n\nfunction encodeRandom(len: number): string {\n // 1 byte = 256 values; we map each to one of 32 base32 chars by\n // taking the low 5 bits. Equivalent randomness density.\n const bytes = randomBytes(len);\n let out = \"\";\n for (let i = 0; i < len; i++) {\n out += ENCODING[bytes[i] & 31];\n }\n return out;\n}\n\n/**\n * Returns a fresh idempotency key of shape `evt_<ULID>`.\n * 48-bit timestamp (millisecond precision) + 80-bit randomness.\n * Sortable lexicographically by creation time.\n */\nexport function generateEventId(now: number = Date.now()): string {\n return `evt_${encodeTime(now, TIME_LEN)}${encodeRandom(RANDOM_LEN)}`;\n}\n","// HMAC-SHA256 signing + verification for the Aim Earn webhook contract.\n// Spec: 2026-04-29-webhook-ingest-design.md §5.\n//\n// Request:\n// X-Aimearn-Signature: t=<unix>,v1=<hex>\n// v1 = HMAC-SHA256(secret, `${t}.${rawBody}`)\n//\n// Response:\n// X-Aimearn-Response-Signature: same shape; same per-brand secret.\n//\n// Brand SDK side: we sign every outbound request and verify every\n// inbound response. Replay window 300s.\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nexport const REPLAY_WINDOW_SECONDS = 300;\n\n/**\n * Builds the `X-Aimearn-Signature` header value for a request body.\n * Caller supplies the raw body bytes — never re-serialize after\n * signing, or the platform's HMAC verify will fail.\n */\nexport function signRequest(\n secret: string,\n rawBody: string,\n now: number = Math.floor(Date.now() / 1000),\n): string {\n const v1 = createHmac(\"sha256\", secret)\n .update(`${now}.${rawBody}`)\n .digest(\"hex\");\n return `t=${now},v1=${v1}`;\n}\n\nexport interface ParsedSignature {\n t: number;\n v1: string;\n}\n\n/**\n * Parses a `t=<unix>,v1=<hex>` header. Returns null on any malformed\n * input — caller treats null as \"missing/unverifiable signature.\"\n */\nexport function parseSignatureHeader(\n header: string | undefined | null,\n): ParsedSignature | null {\n if (!header) return null;\n\n let t: number | null = null;\n let v1: string | null = null;\n for (const part of header.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) return null;\n const k = part.slice(0, eq).trim();\n const v = part.slice(eq + 1).trim();\n if (k === \"t\") {\n const parsed = Number(v);\n if (!Number.isFinite(parsed) || parsed <= 0) return null;\n t = parsed;\n } else if (k === \"v1\") {\n // 64 lowercase hex chars for SHA-256.\n if (!/^[0-9a-f]{64}$/.test(v)) return null;\n v1 = v;\n }\n }\n if (t === null || v1 === null) return null;\n return { t, v1 };\n}\n\n/**\n * Verifies the platform's response signature against the response\n * body bytes. Returns true on match, false on any failure (missing\n * header, stale timestamp, mismatched HMAC). Brand SDK must reject\n * 2xx responses without a verifiable signature; 4xx/5xx may be\n * accepted as platform-level errors per spec §5.5.\n */\nexport function verifyResponseSignature(\n secret: string,\n headerValue: string | undefined | null,\n responseBody: string,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): boolean {\n const sig = parseSignatureHeader(headerValue);\n if (!sig) return false;\n if (Math.abs(nowSeconds - sig.t) > REPLAY_WINDOW_SECONDS) return false;\n\n const expected = createHmac(\"sha256\", secret)\n .update(`${sig.t}.${responseBody}`)\n .digest(\"hex\");\n\n if (expected.length !== sig.v1.length) return false;\n return timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));\n}\n","// Typed error hierarchy mapping platform `WebhookDelivery.result`\n// values onto SDK-side error classes. Brand error handlers can switch\n// on `error.code` (the wire-format result string) or use `instanceof`.\n//\n// Spec: 2026-04-29-webhook-ingest-design.md §10.1 retry contract.\n\nexport type AimearnErrorCode =\n | \"signature_failed\"\n | \"parse_error\"\n | \"duplicate\"\n | \"country_not_published\"\n | \"product_not_found\"\n | \"unknown_referral\"\n | \"discarded\"\n | \"brand_not_found\"\n | \"brand_disabled\"\n | \"missing_key_id\"\n | \"rate_limited\"\n | \"body_too_large\"\n | \"unknown_event_type\"\n | \"unknown_order\"\n | \"brand_mismatch\"\n | \"cancelled_after_shipment\"\n | \"invalid_item_index\"\n | \"subtotal_mismatch\"\n | \"too_many_items\"\n // SDK-side codes for failure modes that don't reach the platform:\n | \"network_error\"\n | \"response_signature_failed\"\n | \"internal_error\";\n\nexport class AimearnError extends Error {\n readonly code: AimearnErrorCode;\n readonly status: number;\n readonly responseBody?: unknown;\n\n constructor(\n code: AimearnErrorCode,\n message: string,\n status: number,\n responseBody?: unknown,\n ) {\n super(message);\n this.name = \"AimearnError\";\n this.code = code;\n this.status = status;\n this.responseBody = responseBody;\n }\n}\n\n/** 401 — signature/auth failure. Brand should fix secret/clock; do NOT retry. */\nexport class AimearnAuthError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 401, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnAuthError\";\n }\n}\n\n/** 400 — payload schema violation. Brand-side bug; do NOT retry. */\nexport class AimearnValidationError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 400, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnValidationError\";\n }\n}\n\n/** 404 — referenced order or brand not found. Brand should re-check IDs; do NOT retry. */\nexport class AimearnNotFoundError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 404, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnNotFoundError\";\n }\n}\n\n/** 403 — brand disabled / brand-mismatch. Escalate to platform admin. */\nexport class AimearnForbiddenError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 403, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnForbiddenError\";\n }\n}\n\n/** 409 — state-machine violation (e.g. order.cancelled after shipment). */\nexport class AimearnConflictError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 409, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnConflictError\";\n }\n}\n\n/** 5xx / network — platform-side error. SDK retries automatically up to the configured limit. */\nexport class AimearnPlatformError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 500, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnPlatformError\";\n }\n}\n\nconst ERROR_CTOR_BY_STATUS: Record<number, typeof AimearnError> = {\n 400: AimearnValidationError,\n 401: AimearnAuthError,\n 403: AimearnForbiddenError,\n 404: AimearnNotFoundError,\n 409: AimearnConflictError,\n 413: AimearnValidationError,\n 429: AimearnPlatformError,\n};\n\n/**\n * Converts an HTTP error response into the right SDK error class.\n * `code` is the platform's `error` field from the response body; falls\n * back to `internal_error` if the body shape is unexpected.\n */\nexport function errorFromResponse(\n status: number,\n body: unknown,\n): AimearnError {\n const errorCode =\n typeof body === \"object\" &&\n body !== null &&\n \"error\" in body &&\n typeof (body as { error: unknown }).error === \"string\"\n ? ((body as { error: AimearnErrorCode }).error)\n : \"internal_error\";\n\n const Ctor =\n ERROR_CTOR_BY_STATUS[status] ??\n (status >= 500 ? AimearnPlatformError : AimearnError);\n\n return new Ctor(\n errorCode,\n `Aim Earn ${status}: ${errorCode}`,\n status,\n body,\n );\n}\n","// Exponential-backoff retry policy for the SDK's outbound POSTs.\n// Spec: 2026-04-29-webhook-ingest-design.md §10.1.\n//\n// 2xx → return result\n// 4xx (except 429) → throw immediately, no retry\n// 429 / 5xx / network → retry with exponential backoff\n//\n// Default: 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, … up to 24 attempts ≈ 24h.\n\nexport interface RetryConfig {\n /** Maximum number of total attempts (including the first). Default 24. */\n maxAttempts?: number;\n /** Base delay in milliseconds. Default 1000. */\n baseDelayMs?: number;\n /** Cap individual delay; default 60_000ms. */\n maxDelayMs?: number;\n /**\n * Hook for tests / cancellation. Receives the milliseconds the SDK\n * intends to sleep; should resolve after that delay (or reject to\n * abort the retry loop).\n */\n sleep?: (ms: number) => Promise<void>;\n}\n\nconst DEFAULTS: Required<RetryConfig> = {\n maxAttempts: 24,\n baseDelayMs: 1000,\n maxDelayMs: 60_000,\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n};\n\nexport function resolveRetryConfig(\n config?: RetryConfig,\n): Required<RetryConfig> {\n return { ...DEFAULTS, ...(config ?? {}) };\n}\n\n/**\n * Returns true if the platform's HTTP status (or the SDK's network-error\n * sentinel of 0) should trigger a retry.\n *\n * - 0 — fetch threw; treat as retriable network failure\n * - 429 — explicit rate-limit; back off and retry\n * - 5xx — platform-side error; retry per spec §10.1\n */\nexport function isRetriable(status: number): boolean {\n return status === 0 || status === 429 || status >= 500;\n}\n\n/**\n * Pure delay-curve calculator. Attempt number is 1-indexed (the\n * first retry — i.e. the 2nd total attempt — uses attempt=2).\n */\nexport function backoffDelayMs(\n attempt: number,\n config: Required<RetryConfig>,\n): number {\n // Exponential: base * 2^(attempt - 1). attempt=1 → no delay (initial\n // call); attempt=2 → base; attempt=3 → 2*base; ...\n if (attempt <= 1) return 0;\n const raw = config.baseDelayMs * 2 ** (attempt - 2);\n return Math.min(raw, config.maxDelayMs);\n}\n","// Aim Earn webhook client. Brand-facing API surface:\n//\n// const client = new AimearnClient({ keyId, secret, endpoint });\n// await client.orderCompleted({ orderId, data: {...} });\n// await client.orderShipped({ orderId, data: {...} });\n// await client.orderRefunded({ orderId, data: {...} });\n// await client.orderCancelled({ orderId, data: {...} });\n//\n// Each method:\n// 1. Builds the envelope (auto-generates eventId via ULID if omitted)\n// 2. Serializes once to a stable string (the same bytes get signed)\n// 3. Signs with HMAC-SHA256 over `${t}.${rawBody}` per spec §5\n// 4. POSTs with X-Aimearn-Key-Id + X-Aimearn-Signature\n// 5. Retries 5xx/429 with exponential backoff up to maxAttempts\n// 6. Verifies the X-Aimearn-Response-Signature on 2xx (throws if missing/invalid)\n// 7. Returns the parsed AcceptedResponse, or throws a typed AimearnError\n\nimport { generateEventId } from \"./eventId\";\nimport { signRequest, verifyResponseSignature } from \"./signing\";\nimport {\n AimearnError,\n AimearnPlatformError,\n errorFromResponse,\n} from \"./errors\";\nimport {\n backoffDelayMs,\n isRetriable,\n resolveRetryConfig,\n type RetryConfig,\n} from \"./retry\";\nimport type {\n AcceptedResponse,\n Envelope,\n OrderCancelledInput,\n OrderCompletedInput,\n OrderRefundedInput,\n OrderShippedInput,\n WireEventType,\n} from \"./types\";\n\nexport interface AimearnClientConfig {\n /** Brand identifier registered with Aim Earn. Sent as X-Aimearn-Key-Id. */\n keyId: string;\n /**\n * Cleartext webhook secret returned by the Aim Earn admin\n * `rotateBrandWebhookSecret` mutation. Used to sign requests and\n * verify response signatures. NEVER log this.\n */\n secret: string;\n /**\n * Full base URL, e.g. `https://webhooks.aimearn.platform.com` or the\n * sandbox auto-generated `https://abc123.execute-api.region.amazonaws.com`.\n */\n endpoint: string;\n /** Optional fetch override for testing / non-Node runtimes. Defaults to global fetch. */\n fetch?: typeof globalThis.fetch;\n retry?: RetryConfig;\n}\n\nconst ROUTE = \"/api/webhooks/order\";\n\nexport class AimearnClient {\n private readonly config: Required<\n Pick<AimearnClientConfig, \"keyId\" | \"secret\" | \"endpoint\">\n > & {\n fetch: typeof globalThis.fetch;\n retry: ReturnType<typeof resolveRetryConfig>;\n };\n\n constructor(config: AimearnClientConfig) {\n if (!config.keyId) throw new Error(\"AimearnClient: keyId is required.\");\n if (!config.secret) throw new Error(\"AimearnClient: secret is required.\");\n if (!config.endpoint) throw new Error(\"AimearnClient: endpoint is required.\");\n\n this.config = {\n keyId: config.keyId,\n secret: config.secret,\n endpoint: config.endpoint.replace(/\\/$/, \"\"),\n fetch: config.fetch ?? globalThis.fetch.bind(globalThis),\n retry: resolveRetryConfig(config.retry),\n };\n }\n\n orderCompleted(input: OrderCompletedInput): Promise<AcceptedResponse> {\n return this.send(\"order.completed\", input);\n }\n\n orderShipped(input: OrderShippedInput): Promise<AcceptedResponse> {\n return this.send(\"order.shipped\", input);\n }\n\n orderRefunded(input: OrderRefundedInput): Promise<AcceptedResponse> {\n return this.send(\"order.refunded\", input);\n }\n\n orderCancelled(input: OrderCancelledInput): Promise<AcceptedResponse> {\n return this.send(\"order.cancelled\", input);\n }\n\n private async send<TData>(\n type: WireEventType,\n input: {\n orderId: string;\n eventId?: string;\n occurredAt?: string;\n data: TData;\n metadata?: Record<string, unknown> | null;\n },\n ): Promise<AcceptedResponse> {\n const envelope: Envelope<TData> = {\n eventId: input.eventId ?? generateEventId(),\n type,\n occurredAt: input.occurredAt ?? new Date().toISOString(),\n orderId: input.orderId,\n data: input.data,\n metadata: input.metadata ?? null,\n };\n const rawBody = JSON.stringify(envelope);\n\n const url = `${this.config.endpoint}${ROUTE}`;\n\n return this.executeWithRetry(url, rawBody);\n }\n\n private async executeWithRetry(\n url: string,\n rawBody: string,\n ): Promise<AcceptedResponse> {\n const { maxAttempts, sleep } = this.config.retry;\n let lastError: AimearnError | undefined;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n const delayMs = backoffDelayMs(attempt, this.config.retry);\n if (delayMs > 0) await sleep(delayMs);\n\n try {\n return await this.attemptOnce(url, rawBody);\n } catch (err) {\n if (!(err instanceof AimearnError)) throw err;\n lastError = err;\n\n if (!isRetriable(err.status)) {\n throw err;\n }\n // Retriable — fall through to next iteration.\n }\n }\n\n throw (\n lastError ??\n new AimearnPlatformError(\n \"internal_error\",\n \"AimearnClient retry budget exhausted with no error captured.\",\n 500,\n )\n );\n }\n\n private async attemptOnce(\n url: string,\n rawBody: string,\n ): Promise<AcceptedResponse> {\n const sigHeader = signRequest(this.config.secret, rawBody);\n\n let response: Response;\n try {\n response = await this.config.fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Aimearn-Key-Id\": this.config.keyId,\n \"X-Aimearn-Signature\": sigHeader,\n },\n body: rawBody,\n });\n } catch (err) {\n // Network failure — retriable per spec §10.1. Wrap as\n // AimearnPlatformError(status:0) so the retry loop catches it.\n throw new AimearnPlatformError(\n \"network_error\",\n `Network error during webhook POST: ${(err as Error).message ?? err}`,\n 0,\n );\n }\n\n const responseBody = await response.text();\n\n // Verify response signature on 2xx — required per spec §5.5 brand\n // contract. Missing/invalid signature on a 2xx is a fatal trust\n // boundary violation.\n if (response.status >= 200 && response.status < 300) {\n const sigOk = verifyResponseSignature(\n this.config.secret,\n response.headers.get(\"x-aimearn-response-signature\"),\n responseBody,\n );\n if (!sigOk) {\n throw new AimearnPlatformError(\n \"response_signature_failed\",\n `Aim Earn response signature missing or invalid (HTTP ${response.status}).`,\n response.status,\n responseBody,\n );\n }\n try {\n return JSON.parse(responseBody) as AcceptedResponse;\n } catch {\n throw new AimearnPlatformError(\n \"internal_error\",\n \"Aim Earn returned 2xx with an unparseable JSON body.\",\n response.status,\n responseBody,\n );\n }\n }\n\n // 4xx / 5xx — parse the error body if possible and throw the\n // typed error class.\n let parsed: unknown = responseBody;\n try {\n parsed = JSON.parse(responseBody);\n } catch {\n // Leave parsed as the raw string.\n }\n throw errorFromResponse(response.status, parsed);\n }\n}\n"],"mappings":"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","names":["import_node_crypto"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/eventId.ts","../src/signing.ts","../src/errors.ts","../src/retry.ts","../src/client.ts"],"sourcesContent":["// @aimearn/webhook-sdk public surface (server entry).\n//\n// Spec: docs/superpowers/specs/2026-04-29-webhook-ingest-design.md\n// Docs: /docs/sdk in the host app\n\nexport const SDK_VERSION = \"1.0.0\";\n\nexport { AimearnClient } from \"./client\";\nexport type { AimearnClientConfig } from \"./client\";\n\nexport { generateEventId } from \"./eventId\";\nexport {\n signRequest,\n verifyResponseSignature,\n parseSignatureHeader,\n REPLAY_WINDOW_SECONDS,\n} from \"./signing\";\n\nexport {\n AimearnError,\n AimearnAuthError,\n AimearnValidationError,\n AimearnNotFoundError,\n AimearnForbiddenError,\n AimearnConflictError,\n AimearnPlatformError,\n errorFromResponse,\n} from \"./errors\";\nexport type { AimearnErrorCode } from \"./errors\";\n\nexport { isRetriable, backoffDelayMs, resolveRetryConfig } from \"./retry\";\nexport type { RetryConfig } from \"./retry\";\n\nexport type {\n WireEventType,\n CompletedItem,\n CustomerInfo,\n CompletedData,\n ShippedData,\n RefundedData,\n CancelledData,\n Envelope,\n OrderCompletedInput,\n OrderShippedInput,\n OrderRefundedInput,\n OrderCancelledInput,\n AcceptedResponse,\n} from \"./types\";\n","// Stripe-style ULID-shaped idempotency key, formatted as\n// `evt_<26-char-base32-crockford>`. Same external shape as our backend\n// uses, but generated client-side so brands can collapse retries from\n// the same call site naturally — re-issuing the same `eventId` against\n// the platform is idempotent (the ingest Lambda's\n// `attribute_not_exists(orderId)` guard catches duplicates).\n//\n// Deliberately not pulling in the `ulid` npm package — keeping the\n// SDK dependency-free at runtime keeps the install size small and\n// avoids supply-chain surface. The implementation below is a faithful\n// 48-bit-time + 80-bit-randomness ULID with Crockford base32, matching\n// the spec at https://github.com/ulid/spec.\n\nimport { randomBytes } from \"node:crypto\";\n\nconst ENCODING = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\"; // Crockford base32\nconst ENCODING_LEN = ENCODING.length;\nconst TIME_LEN = 10;\nconst RANDOM_LEN = 16;\n\nfunction encodeTime(now: number, len: number): string {\n let out = \"\";\n let n = now;\n for (let i = len - 1; i >= 0; i--) {\n const mod = n % ENCODING_LEN;\n out = ENCODING[mod] + out;\n n = (n - mod) / ENCODING_LEN;\n }\n return out;\n}\n\nfunction encodeRandom(len: number): string {\n // 1 byte = 256 values; we map each to one of 32 base32 chars by\n // taking the low 5 bits. Equivalent randomness density.\n const bytes = randomBytes(len);\n let out = \"\";\n for (let i = 0; i < len; i++) {\n out += ENCODING[bytes[i] & 31];\n }\n return out;\n}\n\n/**\n * Returns a fresh idempotency key of shape `evt_<ULID>`.\n * 48-bit timestamp (millisecond precision) + 80-bit randomness.\n * Sortable lexicographically by creation time.\n */\nexport function generateEventId(now: number = Date.now()): string {\n return `evt_${encodeTime(now, TIME_LEN)}${encodeRandom(RANDOM_LEN)}`;\n}\n","// HMAC-SHA256 signing + verification for the Aim Earn webhook contract.\n// Spec: 2026-04-29-webhook-ingest-design.md §5.\n//\n// Request:\n// X-Aimearn-Signature: t=<unix>,v1=<hex>\n// v1 = HMAC-SHA256(secret, `${t}.${rawBody}`)\n//\n// Response:\n// X-Aimearn-Response-Signature: same shape; same per-brand secret.\n//\n// Brand SDK side: we sign every outbound request and verify every\n// inbound response. Replay window 300s.\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nexport const REPLAY_WINDOW_SECONDS = 300;\n\n/**\n * Builds the `X-Aimearn-Signature` header value for a request body.\n * Caller supplies the raw body bytes — never re-serialize after\n * signing, or the platform's HMAC verify will fail.\n */\nexport function signRequest(\n secret: string,\n rawBody: string,\n now: number = Math.floor(Date.now() / 1000),\n): string {\n const v1 = createHmac(\"sha256\", secret)\n .update(`${now}.${rawBody}`)\n .digest(\"hex\");\n return `t=${now},v1=${v1}`;\n}\n\nexport interface ParsedSignature {\n t: number;\n v1: string;\n}\n\n/**\n * Parses a `t=<unix>,v1=<hex>` header. Returns null on any malformed\n * input — caller treats null as \"missing/unverifiable signature.\"\n */\nexport function parseSignatureHeader(\n header: string | undefined | null,\n): ParsedSignature | null {\n if (!header) return null;\n\n let t: number | null = null;\n let v1: string | null = null;\n for (const part of header.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) return null;\n const k = part.slice(0, eq).trim();\n const v = part.slice(eq + 1).trim();\n if (k === \"t\") {\n const parsed = Number(v);\n if (!Number.isFinite(parsed) || parsed <= 0) return null;\n t = parsed;\n } else if (k === \"v1\") {\n // 64 lowercase hex chars for SHA-256.\n if (!/^[0-9a-f]{64}$/.test(v)) return null;\n v1 = v;\n }\n }\n if (t === null || v1 === null) return null;\n return { t, v1 };\n}\n\n/**\n * Verifies the platform's response signature against the response\n * body bytes. Returns true on match, false on any failure (missing\n * header, stale timestamp, mismatched HMAC). Brand SDK must reject\n * 2xx responses without a verifiable signature; 4xx/5xx may be\n * accepted as platform-level errors per spec §5.5.\n */\nexport function verifyResponseSignature(\n secret: string,\n headerValue: string | undefined | null,\n responseBody: string,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): boolean {\n const sig = parseSignatureHeader(headerValue);\n if (!sig) return false;\n if (Math.abs(nowSeconds - sig.t) > REPLAY_WINDOW_SECONDS) return false;\n\n const expected = createHmac(\"sha256\", secret)\n .update(`${sig.t}.${responseBody}`)\n .digest(\"hex\");\n\n if (expected.length !== sig.v1.length) return false;\n return timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));\n}\n","// Typed error hierarchy mapping platform `WebhookDelivery.result`\n// values onto SDK-side error classes. Brand error handlers can switch\n// on `error.code` (the wire-format result string) or use `instanceof`.\n//\n// Spec: 2026-04-29-webhook-ingest-design.md §10.1 retry contract.\n\nexport type AimearnErrorCode =\n | \"signature_failed\"\n | \"parse_error\"\n | \"duplicate\"\n | \"country_not_published\"\n | \"product_not_found\"\n | \"unknown_referral\"\n | \"discarded\"\n | \"brand_not_found\"\n | \"brand_disabled\"\n | \"missing_key_id\"\n | \"rate_limited\"\n | \"body_too_large\"\n | \"unknown_event_type\"\n | \"unknown_order\"\n | \"brand_mismatch\"\n | \"cancelled_after_shipment\"\n | \"invalid_item_index\"\n | \"subtotal_mismatch\"\n | \"too_many_items\"\n // SDK-side codes for failure modes that don't reach the platform:\n | \"network_error\"\n | \"response_signature_failed\"\n | \"internal_error\";\n\nexport class AimearnError extends Error {\n readonly code: AimearnErrorCode;\n readonly status: number;\n readonly responseBody?: unknown;\n\n constructor(\n code: AimearnErrorCode,\n message: string,\n status: number,\n responseBody?: unknown,\n ) {\n super(message);\n this.name = \"AimearnError\";\n this.code = code;\n this.status = status;\n this.responseBody = responseBody;\n }\n}\n\n/** 401 — signature/auth failure. Brand should fix secret/clock; do NOT retry. */\nexport class AimearnAuthError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 401, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnAuthError\";\n }\n}\n\n/** 400 — payload schema violation. Brand-side bug; do NOT retry. */\nexport class AimearnValidationError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 400, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnValidationError\";\n }\n}\n\n/** 404 — referenced order or brand not found. Brand should re-check IDs; do NOT retry. */\nexport class AimearnNotFoundError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 404, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnNotFoundError\";\n }\n}\n\n/** 403 — brand disabled / brand-mismatch. Escalate to platform admin. */\nexport class AimearnForbiddenError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 403, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnForbiddenError\";\n }\n}\n\n/** 409 — state-machine violation (e.g. order.cancelled after shipment). */\nexport class AimearnConflictError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 409, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnConflictError\";\n }\n}\n\n/** 5xx / network — platform-side error. SDK retries automatically up to the configured limit. */\nexport class AimearnPlatformError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 500, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnPlatformError\";\n }\n}\n\nconst ERROR_CTOR_BY_STATUS: Record<number, typeof AimearnError> = {\n 400: AimearnValidationError,\n 401: AimearnAuthError,\n 403: AimearnForbiddenError,\n 404: AimearnNotFoundError,\n 409: AimearnConflictError,\n 413: AimearnValidationError,\n 429: AimearnPlatformError,\n};\n\n/**\n * Converts an HTTP error response into the right SDK error class.\n * `code` is the platform's `error` field from the response body; falls\n * back to `internal_error` if the body shape is unexpected.\n */\nexport function errorFromResponse(\n status: number,\n body: unknown,\n): AimearnError {\n const errorCode =\n typeof body === \"object\" &&\n body !== null &&\n \"error\" in body &&\n typeof (body as { error: unknown }).error === \"string\"\n ? ((body as { error: AimearnErrorCode }).error)\n : \"internal_error\";\n\n const Ctor =\n ERROR_CTOR_BY_STATUS[status] ??\n (status >= 500 ? AimearnPlatformError : AimearnError);\n\n return new Ctor(\n errorCode,\n `Aim Earn ${status}: ${errorCode}`,\n status,\n body,\n );\n}\n","// Exponential-backoff retry policy for the SDK's outbound POSTs.\n// Spec: 2026-04-29-webhook-ingest-design.md §10.1.\n//\n// 2xx → return result\n// 4xx (except 429) → throw immediately, no retry\n// 429 / 5xx / network → retry with exponential backoff\n//\n// Default: 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, … up to 24 attempts ≈ 24h.\n\nexport interface RetryConfig {\n /** Maximum number of total attempts (including the first). Default 24. */\n maxAttempts?: number;\n /** Base delay in milliseconds. Default 1000. */\n baseDelayMs?: number;\n /** Cap individual delay; default 60_000ms. */\n maxDelayMs?: number;\n /**\n * Hook for tests / cancellation. Receives the milliseconds the SDK\n * intends to sleep; should resolve after that delay (or reject to\n * abort the retry loop).\n */\n sleep?: (ms: number) => Promise<void>;\n}\n\nconst DEFAULTS: Required<RetryConfig> = {\n maxAttempts: 24,\n baseDelayMs: 1000,\n maxDelayMs: 60_000,\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n};\n\nexport function resolveRetryConfig(\n config?: RetryConfig,\n): Required<RetryConfig> {\n return { ...DEFAULTS, ...(config ?? {}) };\n}\n\n/**\n * Returns true if the platform's HTTP status (or the SDK's network-error\n * sentinel of 0) should trigger a retry.\n *\n * - 0 — fetch threw; treat as retriable network failure\n * - 429 — explicit rate-limit; back off and retry\n * - 5xx — platform-side error; retry per spec §10.1\n */\nexport function isRetriable(status: number): boolean {\n return status === 0 || status === 429 || status >= 500;\n}\n\n/**\n * Pure delay-curve calculator. Attempt number is 1-indexed (the\n * first retry — i.e. the 2nd total attempt — uses attempt=2).\n */\nexport function backoffDelayMs(\n attempt: number,\n config: Required<RetryConfig>,\n): number {\n // Exponential: base * 2^(attempt - 1). attempt=1 → no delay (initial\n // call); attempt=2 → base; attempt=3 → 2*base; ...\n if (attempt <= 1) return 0;\n const raw = config.baseDelayMs * 2 ** (attempt - 2);\n return Math.min(raw, config.maxDelayMs);\n}\n","// Aim Earn webhook client. Brand-facing API surface:\n//\n// const client = new AimearnClient({ keyId, secret, endpoint });\n// await client.orderCompleted({ orderId, data: {...} });\n// await client.orderShipped({ orderId, data: {...} });\n// await client.orderRefunded({ orderId, data: {...} });\n// await client.orderCancelled({ orderId, data: {...} });\n//\n// Each method:\n// 1. Builds the envelope (auto-generates eventId via ULID if omitted)\n// 2. Serializes once to a stable string (the same bytes get signed)\n// 3. Signs with HMAC-SHA256 over `${t}.${rawBody}` per spec §5\n// 4. POSTs with X-Aimearn-Key-Id + X-Aimearn-Signature\n// 5. Retries 5xx/429 with exponential backoff up to maxAttempts\n// 6. Verifies the X-Aimearn-Response-Signature on 2xx (throws if missing/invalid)\n// 7. Returns the parsed AcceptedResponse, or throws a typed AimearnError\n\nimport { generateEventId } from \"./eventId\";\nimport { signRequest, verifyResponseSignature } from \"./signing\";\nimport {\n AimearnError,\n AimearnPlatformError,\n errorFromResponse,\n} from \"./errors\";\nimport {\n backoffDelayMs,\n isRetriable,\n resolveRetryConfig,\n type RetryConfig,\n} from \"./retry\";\nimport type {\n AcceptedResponse,\n Envelope,\n OrderCancelledInput,\n OrderCompletedInput,\n OrderRefundedInput,\n OrderShippedInput,\n WireEventType,\n} from \"./types\";\n\nexport interface AimearnClientConfig {\n /** Brand identifier registered with Aim Earn. Sent as X-Aimearn-Key-Id. */\n keyId: string;\n /**\n * Cleartext webhook secret returned by the Aim Earn admin\n * `rotateBrandWebhookSecret` mutation. Used to sign requests and\n * verify response signatures. NEVER log this.\n */\n secret: string;\n /**\n * Full base URL, e.g. `https://webhooks.aimearn.platform.com` or the\n * sandbox auto-generated `https://abc123.execute-api.region.amazonaws.com`.\n */\n endpoint: string;\n /** Optional fetch override for testing / non-Node runtimes. Defaults to global fetch. */\n fetch?: typeof globalThis.fetch;\n retry?: RetryConfig;\n}\n\nconst ROUTE = \"/api/webhooks/order\";\n\nexport class AimearnClient {\n private readonly config: Required<\n Pick<AimearnClientConfig, \"keyId\" | \"secret\" | \"endpoint\">\n > & {\n fetch: typeof globalThis.fetch;\n retry: ReturnType<typeof resolveRetryConfig>;\n };\n\n constructor(config: AimearnClientConfig) {\n if (!config.keyId) throw new Error(\"AimearnClient: keyId is required.\");\n if (!config.secret) throw new Error(\"AimearnClient: secret is required.\");\n if (!config.endpoint) throw new Error(\"AimearnClient: endpoint is required.\");\n\n this.config = {\n keyId: config.keyId,\n secret: config.secret,\n endpoint: config.endpoint.replace(/\\/$/, \"\"),\n fetch: config.fetch ?? globalThis.fetch.bind(globalThis),\n retry: resolveRetryConfig(config.retry),\n };\n }\n\n orderCompleted(input: OrderCompletedInput): Promise<AcceptedResponse> {\n return this.send(\"order.completed\", input);\n }\n\n orderShipped(input: OrderShippedInput): Promise<AcceptedResponse> {\n return this.send(\"order.shipped\", input);\n }\n\n orderRefunded(input: OrderRefundedInput): Promise<AcceptedResponse> {\n return this.send(\"order.refunded\", input);\n }\n\n orderCancelled(input: OrderCancelledInput): Promise<AcceptedResponse> {\n return this.send(\"order.cancelled\", input);\n }\n\n private async send<TData>(\n type: WireEventType,\n input: {\n orderId: string;\n eventId?: string;\n occurredAt?: string;\n data: TData;\n metadata?: Record<string, unknown> | null;\n },\n ): Promise<AcceptedResponse> {\n const envelope: Envelope<TData> = {\n eventId: input.eventId ?? generateEventId(),\n type,\n occurredAt: input.occurredAt ?? new Date().toISOString(),\n orderId: input.orderId,\n data: input.data,\n metadata: input.metadata ?? null,\n };\n const rawBody = JSON.stringify(envelope);\n\n const url = `${this.config.endpoint}${ROUTE}`;\n\n return this.executeWithRetry(url, rawBody);\n }\n\n private async executeWithRetry(\n url: string,\n rawBody: string,\n ): Promise<AcceptedResponse> {\n const { maxAttempts, sleep } = this.config.retry;\n let lastError: AimearnError | undefined;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n const delayMs = backoffDelayMs(attempt, this.config.retry);\n if (delayMs > 0) await sleep(delayMs);\n\n try {\n return await this.attemptOnce(url, rawBody);\n } catch (err) {\n if (!(err instanceof AimearnError)) throw err;\n lastError = err;\n\n if (!isRetriable(err.status)) {\n throw err;\n }\n // Retriable — fall through to next iteration.\n }\n }\n\n throw (\n lastError ??\n new AimearnPlatformError(\n \"internal_error\",\n \"AimearnClient retry budget exhausted with no error captured.\",\n 500,\n )\n );\n }\n\n private async attemptOnce(\n url: string,\n rawBody: string,\n ): Promise<AcceptedResponse> {\n const sigHeader = signRequest(this.config.secret, rawBody);\n\n let response: Response;\n try {\n response = await this.config.fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Aimearn-Key-Id\": this.config.keyId,\n \"X-Aimearn-Signature\": sigHeader,\n },\n body: rawBody,\n });\n } catch (err) {\n // Network failure — retriable per spec §10.1. Wrap as\n // AimearnPlatformError(status:0) so the retry loop catches it.\n throw new AimearnPlatformError(\n \"network_error\",\n `Network error during webhook POST: ${(err as Error).message ?? err}`,\n 0,\n );\n }\n\n const responseBody = await response.text();\n\n // Verify response signature on 2xx — required per spec §5.5 brand\n // contract. Missing/invalid signature on a 2xx is a fatal trust\n // boundary violation.\n if (response.status >= 200 && response.status < 300) {\n const sigOk = verifyResponseSignature(\n this.config.secret,\n response.headers.get(\"x-aimearn-response-signature\"),\n responseBody,\n );\n if (!sigOk) {\n throw new AimearnPlatformError(\n \"response_signature_failed\",\n `Aim Earn response signature missing or invalid (HTTP ${response.status}).`,\n response.status,\n responseBody,\n );\n }\n try {\n return JSON.parse(responseBody) as AcceptedResponse;\n } catch {\n throw new AimearnPlatformError(\n \"internal_error\",\n \"Aim Earn returned 2xx with an unparseable JSON body.\",\n response.status,\n responseBody,\n );\n }\n }\n\n // 4xx / 5xx — parse the error body if possible and throw the\n // typed error class.\n let parsed: unknown = responseBody;\n try {\n parsed = JSON.parse(responseBody);\n } catch {\n // Leave parsed as the raw string.\n }\n throw errorFromResponse(response.status, parsed);\n }\n}\n"],"mappings":"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","names":["import_node_crypto"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -39,6 +39,14 @@ interface CompletedItem {
|
|
|
39
39
|
productLink?: string | null;
|
|
40
40
|
quantity: number;
|
|
41
41
|
unitPriceCents: number;
|
|
42
|
+
/**
|
|
43
|
+
* The brand-computed commission for this line item, in cents.
|
|
44
|
+
* **Required.** The platform uses this value (rather than deriving
|
|
45
|
+
* commission from `subtotalCents`) and splits it across the affiliate
|
|
46
|
+
* chain using the configured `ProductCountry` percentages as ratios.
|
|
47
|
+
* Compute it post-discount/promotion on your side before sending.
|
|
48
|
+
*/
|
|
49
|
+
commissionCents: number;
|
|
42
50
|
}
|
|
43
51
|
interface CustomerInfo {
|
|
44
52
|
name?: string | null;
|
|
@@ -65,6 +73,16 @@ interface CompletedData {
|
|
|
65
73
|
* order-pipeline auth model.
|
|
66
74
|
*/
|
|
67
75
|
customer?: CustomerInfo | null;
|
|
76
|
+
/**
|
|
77
|
+
* The brand-computed total commission for the order, in cents.
|
|
78
|
+
* **Required.** The platform uses brand-provided commission figures
|
|
79
|
+
* (rather than deriving from `subtotalCents`): each item's
|
|
80
|
+
* `commissionCents` is split across the affiliate chain using the
|
|
81
|
+
* configured `ProductCountry` percentages as ratios.
|
|
82
|
+
* If `Σ item.commissionCents !== totalCommissionCents`, the order is
|
|
83
|
+
* accepted with a `commission_mismatch` warning.
|
|
84
|
+
*/
|
|
85
|
+
totalCommissionCents: number;
|
|
68
86
|
metadata?: Record<string, unknown> | null;
|
|
69
87
|
}
|
|
70
88
|
interface ShippedData {
|
|
@@ -241,6 +259,6 @@ declare class AimearnPlatformError extends AimearnError {
|
|
|
241
259
|
*/
|
|
242
260
|
declare function errorFromResponse(status: number, body: unknown): AimearnError;
|
|
243
261
|
|
|
244
|
-
declare const SDK_VERSION = "
|
|
262
|
+
declare const SDK_VERSION = "1.0.0";
|
|
245
263
|
|
|
246
264
|
export { type AcceptedResponse, AimearnAuthError, AimearnClient, type AimearnClientConfig, AimearnConflictError, AimearnError, type AimearnErrorCode, AimearnForbiddenError, AimearnNotFoundError, AimearnPlatformError, AimearnValidationError, type CancelledData, type CompletedData, type CompletedItem, type CustomerInfo, type Envelope, type OrderCancelledInput, type OrderCompletedInput, type OrderRefundedInput, type OrderShippedInput, REPLAY_WINDOW_SECONDS, type RefundedData, type RetryConfig, SDK_VERSION, type ShippedData, type WireEventType, backoffDelayMs, errorFromResponse, generateEventId, isRetriable, parseSignatureHeader, resolveRetryConfig, signRequest, verifyResponseSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ interface CompletedItem {
|
|
|
39
39
|
productLink?: string | null;
|
|
40
40
|
quantity: number;
|
|
41
41
|
unitPriceCents: number;
|
|
42
|
+
/**
|
|
43
|
+
* The brand-computed commission for this line item, in cents.
|
|
44
|
+
* **Required.** The platform uses this value (rather than deriving
|
|
45
|
+
* commission from `subtotalCents`) and splits it across the affiliate
|
|
46
|
+
* chain using the configured `ProductCountry` percentages as ratios.
|
|
47
|
+
* Compute it post-discount/promotion on your side before sending.
|
|
48
|
+
*/
|
|
49
|
+
commissionCents: number;
|
|
42
50
|
}
|
|
43
51
|
interface CustomerInfo {
|
|
44
52
|
name?: string | null;
|
|
@@ -65,6 +73,16 @@ interface CompletedData {
|
|
|
65
73
|
* order-pipeline auth model.
|
|
66
74
|
*/
|
|
67
75
|
customer?: CustomerInfo | null;
|
|
76
|
+
/**
|
|
77
|
+
* The brand-computed total commission for the order, in cents.
|
|
78
|
+
* **Required.** The platform uses brand-provided commission figures
|
|
79
|
+
* (rather than deriving from `subtotalCents`): each item's
|
|
80
|
+
* `commissionCents` is split across the affiliate chain using the
|
|
81
|
+
* configured `ProductCountry` percentages as ratios.
|
|
82
|
+
* If `Σ item.commissionCents !== totalCommissionCents`, the order is
|
|
83
|
+
* accepted with a `commission_mismatch` warning.
|
|
84
|
+
*/
|
|
85
|
+
totalCommissionCents: number;
|
|
68
86
|
metadata?: Record<string, unknown> | null;
|
|
69
87
|
}
|
|
70
88
|
interface ShippedData {
|
|
@@ -241,6 +259,6 @@ declare class AimearnPlatformError extends AimearnError {
|
|
|
241
259
|
*/
|
|
242
260
|
declare function errorFromResponse(status: number, body: unknown): AimearnError;
|
|
243
261
|
|
|
244
|
-
declare const SDK_VERSION = "
|
|
262
|
+
declare const SDK_VERSION = "1.0.0";
|
|
245
263
|
|
|
246
264
|
export { type AcceptedResponse, AimearnAuthError, AimearnClient, type AimearnClientConfig, AimearnConflictError, AimearnError, type AimearnErrorCode, AimearnForbiddenError, AimearnNotFoundError, AimearnPlatformError, AimearnValidationError, type CancelledData, type CompletedData, type CompletedItem, type CustomerInfo, type Envelope, type OrderCancelledInput, type OrderCompletedInput, type OrderRefundedInput, type OrderShippedInput, REPLAY_WINDOW_SECONDS, type RefundedData, type RetryConfig, SDK_VERSION, type ShippedData, type WireEventType, backoffDelayMs, errorFromResponse, generateEventId, isRetriable, parseSignatureHeader, resolveRetryConfig, signRequest, verifyResponseSignature };
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/eventId.ts","../src/signing.ts","../src/errors.ts","../src/retry.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["// Stripe-style ULID-shaped idempotency key, formatted as\n// `evt_<26-char-base32-crockford>`. Same external shape as our backend\n// uses, but generated client-side so brands can collapse retries from\n// the same call site naturally — re-issuing the same `eventId` against\n// the platform is idempotent (the ingest Lambda's\n// `attribute_not_exists(orderId)` guard catches duplicates).\n//\n// Deliberately not pulling in the `ulid` npm package — keeping the\n// SDK dependency-free at runtime keeps the install size small and\n// avoids supply-chain surface. The implementation below is a faithful\n// 48-bit-time + 80-bit-randomness ULID with Crockford base32, matching\n// the spec at https://github.com/ulid/spec.\n\nimport { randomBytes } from \"node:crypto\";\n\nconst ENCODING = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\"; // Crockford base32\nconst ENCODING_LEN = ENCODING.length;\nconst TIME_LEN = 10;\nconst RANDOM_LEN = 16;\n\nfunction encodeTime(now: number, len: number): string {\n let out = \"\";\n let n = now;\n for (let i = len - 1; i >= 0; i--) {\n const mod = n % ENCODING_LEN;\n out = ENCODING[mod] + out;\n n = (n - mod) / ENCODING_LEN;\n }\n return out;\n}\n\nfunction encodeRandom(len: number): string {\n // 1 byte = 256 values; we map each to one of 32 base32 chars by\n // taking the low 5 bits. Equivalent randomness density.\n const bytes = randomBytes(len);\n let out = \"\";\n for (let i = 0; i < len; i++) {\n out += ENCODING[bytes[i] & 31];\n }\n return out;\n}\n\n/**\n * Returns a fresh idempotency key of shape `evt_<ULID>`.\n * 48-bit timestamp (millisecond precision) + 80-bit randomness.\n * Sortable lexicographically by creation time.\n */\nexport function generateEventId(now: number = Date.now()): string {\n return `evt_${encodeTime(now, TIME_LEN)}${encodeRandom(RANDOM_LEN)}`;\n}\n","// HMAC-SHA256 signing + verification for the Aim Earn webhook contract.\n// Spec: 2026-04-29-webhook-ingest-design.md §5.\n//\n// Request:\n// X-Aimearn-Signature: t=<unix>,v1=<hex>\n// v1 = HMAC-SHA256(secret, `${t}.${rawBody}`)\n//\n// Response:\n// X-Aimearn-Response-Signature: same shape; same per-brand secret.\n//\n// Brand SDK side: we sign every outbound request and verify every\n// inbound response. Replay window 300s.\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nexport const REPLAY_WINDOW_SECONDS = 300;\n\n/**\n * Builds the `X-Aimearn-Signature` header value for a request body.\n * Caller supplies the raw body bytes — never re-serialize after\n * signing, or the platform's HMAC verify will fail.\n */\nexport function signRequest(\n secret: string,\n rawBody: string,\n now: number = Math.floor(Date.now() / 1000),\n): string {\n const v1 = createHmac(\"sha256\", secret)\n .update(`${now}.${rawBody}`)\n .digest(\"hex\");\n return `t=${now},v1=${v1}`;\n}\n\nexport interface ParsedSignature {\n t: number;\n v1: string;\n}\n\n/**\n * Parses a `t=<unix>,v1=<hex>` header. Returns null on any malformed\n * input — caller treats null as \"missing/unverifiable signature.\"\n */\nexport function parseSignatureHeader(\n header: string | undefined | null,\n): ParsedSignature | null {\n if (!header) return null;\n\n let t: number | null = null;\n let v1: string | null = null;\n for (const part of header.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) return null;\n const k = part.slice(0, eq).trim();\n const v = part.slice(eq + 1).trim();\n if (k === \"t\") {\n const parsed = Number(v);\n if (!Number.isFinite(parsed) || parsed <= 0) return null;\n t = parsed;\n } else if (k === \"v1\") {\n // 64 lowercase hex chars for SHA-256.\n if (!/^[0-9a-f]{64}$/.test(v)) return null;\n v1 = v;\n }\n }\n if (t === null || v1 === null) return null;\n return { t, v1 };\n}\n\n/**\n * Verifies the platform's response signature against the response\n * body bytes. Returns true on match, false on any failure (missing\n * header, stale timestamp, mismatched HMAC). Brand SDK must reject\n * 2xx responses without a verifiable signature; 4xx/5xx may be\n * accepted as platform-level errors per spec §5.5.\n */\nexport function verifyResponseSignature(\n secret: string,\n headerValue: string | undefined | null,\n responseBody: string,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): boolean {\n const sig = parseSignatureHeader(headerValue);\n if (!sig) return false;\n if (Math.abs(nowSeconds - sig.t) > REPLAY_WINDOW_SECONDS) return false;\n\n const expected = createHmac(\"sha256\", secret)\n .update(`${sig.t}.${responseBody}`)\n .digest(\"hex\");\n\n if (expected.length !== sig.v1.length) return false;\n return timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));\n}\n","// Typed error hierarchy mapping platform `WebhookDelivery.result`\n// values onto SDK-side error classes. Brand error handlers can switch\n// on `error.code` (the wire-format result string) or use `instanceof`.\n//\n// Spec: 2026-04-29-webhook-ingest-design.md §10.1 retry contract.\n\nexport type AimearnErrorCode =\n | \"signature_failed\"\n | \"parse_error\"\n | \"duplicate\"\n | \"country_not_published\"\n | \"product_not_found\"\n | \"unknown_referral\"\n | \"discarded\"\n | \"brand_not_found\"\n | \"brand_disabled\"\n | \"missing_key_id\"\n | \"rate_limited\"\n | \"body_too_large\"\n | \"unknown_event_type\"\n | \"unknown_order\"\n | \"brand_mismatch\"\n | \"cancelled_after_shipment\"\n | \"invalid_item_index\"\n | \"subtotal_mismatch\"\n | \"too_many_items\"\n // SDK-side codes for failure modes that don't reach the platform:\n | \"network_error\"\n | \"response_signature_failed\"\n | \"internal_error\";\n\nexport class AimearnError extends Error {\n readonly code: AimearnErrorCode;\n readonly status: number;\n readonly responseBody?: unknown;\n\n constructor(\n code: AimearnErrorCode,\n message: string,\n status: number,\n responseBody?: unknown,\n ) {\n super(message);\n this.name = \"AimearnError\";\n this.code = code;\n this.status = status;\n this.responseBody = responseBody;\n }\n}\n\n/** 401 — signature/auth failure. Brand should fix secret/clock; do NOT retry. */\nexport class AimearnAuthError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 401, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnAuthError\";\n }\n}\n\n/** 400 — payload schema violation. Brand-side bug; do NOT retry. */\nexport class AimearnValidationError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 400, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnValidationError\";\n }\n}\n\n/** 404 — referenced order or brand not found. Brand should re-check IDs; do NOT retry. */\nexport class AimearnNotFoundError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 404, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnNotFoundError\";\n }\n}\n\n/** 403 — brand disabled / brand-mismatch. Escalate to platform admin. */\nexport class AimearnForbiddenError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 403, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnForbiddenError\";\n }\n}\n\n/** 409 — state-machine violation (e.g. order.cancelled after shipment). */\nexport class AimearnConflictError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 409, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnConflictError\";\n }\n}\n\n/** 5xx / network — platform-side error. SDK retries automatically up to the configured limit. */\nexport class AimearnPlatformError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 500, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnPlatformError\";\n }\n}\n\nconst ERROR_CTOR_BY_STATUS: Record<number, typeof AimearnError> = {\n 400: AimearnValidationError,\n 401: AimearnAuthError,\n 403: AimearnForbiddenError,\n 404: AimearnNotFoundError,\n 409: AimearnConflictError,\n 413: AimearnValidationError,\n 429: AimearnPlatformError,\n};\n\n/**\n * Converts an HTTP error response into the right SDK error class.\n * `code` is the platform's `error` field from the response body; falls\n * back to `internal_error` if the body shape is unexpected.\n */\nexport function errorFromResponse(\n status: number,\n body: unknown,\n): AimearnError {\n const errorCode =\n typeof body === \"object\" &&\n body !== null &&\n \"error\" in body &&\n typeof (body as { error: unknown }).error === \"string\"\n ? ((body as { error: AimearnErrorCode }).error)\n : \"internal_error\";\n\n const Ctor =\n ERROR_CTOR_BY_STATUS[status] ??\n (status >= 500 ? AimearnPlatformError : AimearnError);\n\n return new Ctor(\n errorCode,\n `Aim Earn ${status}: ${errorCode}`,\n status,\n body,\n );\n}\n","// Exponential-backoff retry policy for the SDK's outbound POSTs.\n// Spec: 2026-04-29-webhook-ingest-design.md §10.1.\n//\n// 2xx → return result\n// 4xx (except 429) → throw immediately, no retry\n// 429 / 5xx / network → retry with exponential backoff\n//\n// Default: 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, … up to 24 attempts ≈ 24h.\n\nexport interface RetryConfig {\n /** Maximum number of total attempts (including the first). Default 24. */\n maxAttempts?: number;\n /** Base delay in milliseconds. Default 1000. */\n baseDelayMs?: number;\n /** Cap individual delay; default 60_000ms. */\n maxDelayMs?: number;\n /**\n * Hook for tests / cancellation. Receives the milliseconds the SDK\n * intends to sleep; should resolve after that delay (or reject to\n * abort the retry loop).\n */\n sleep?: (ms: number) => Promise<void>;\n}\n\nconst DEFAULTS: Required<RetryConfig> = {\n maxAttempts: 24,\n baseDelayMs: 1000,\n maxDelayMs: 60_000,\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n};\n\nexport function resolveRetryConfig(\n config?: RetryConfig,\n): Required<RetryConfig> {\n return { ...DEFAULTS, ...(config ?? {}) };\n}\n\n/**\n * Returns true if the platform's HTTP status (or the SDK's network-error\n * sentinel of 0) should trigger a retry.\n *\n * - 0 — fetch threw; treat as retriable network failure\n * - 429 — explicit rate-limit; back off and retry\n * - 5xx — platform-side error; retry per spec §10.1\n */\nexport function isRetriable(status: number): boolean {\n return status === 0 || status === 429 || status >= 500;\n}\n\n/**\n * Pure delay-curve calculator. Attempt number is 1-indexed (the\n * first retry — i.e. the 2nd total attempt — uses attempt=2).\n */\nexport function backoffDelayMs(\n attempt: number,\n config: Required<RetryConfig>,\n): number {\n // Exponential: base * 2^(attempt - 1). attempt=1 → no delay (initial\n // call); attempt=2 → base; attempt=3 → 2*base; ...\n if (attempt <= 1) return 0;\n const raw = config.baseDelayMs * 2 ** (attempt - 2);\n return Math.min(raw, config.maxDelayMs);\n}\n","// Aim Earn webhook client. Brand-facing API surface:\n//\n// const client = new AimearnClient({ keyId, secret, endpoint });\n// await client.orderCompleted({ orderId, data: {...} });\n// await client.orderShipped({ orderId, data: {...} });\n// await client.orderRefunded({ orderId, data: {...} });\n// await client.orderCancelled({ orderId, data: {...} });\n//\n// Each method:\n// 1. Builds the envelope (auto-generates eventId via ULID if omitted)\n// 2. Serializes once to a stable string (the same bytes get signed)\n// 3. Signs with HMAC-SHA256 over `${t}.${rawBody}` per spec §5\n// 4. POSTs with X-Aimearn-Key-Id + X-Aimearn-Signature\n// 5. Retries 5xx/429 with exponential backoff up to maxAttempts\n// 6. Verifies the X-Aimearn-Response-Signature on 2xx (throws if missing/invalid)\n// 7. Returns the parsed AcceptedResponse, or throws a typed AimearnError\n\nimport { generateEventId } from \"./eventId\";\nimport { signRequest, verifyResponseSignature } from \"./signing\";\nimport {\n AimearnError,\n AimearnPlatformError,\n errorFromResponse,\n} from \"./errors\";\nimport {\n backoffDelayMs,\n isRetriable,\n resolveRetryConfig,\n type RetryConfig,\n} from \"./retry\";\nimport type {\n AcceptedResponse,\n Envelope,\n OrderCancelledInput,\n OrderCompletedInput,\n OrderRefundedInput,\n OrderShippedInput,\n WireEventType,\n} from \"./types\";\n\nexport interface AimearnClientConfig {\n /** Brand identifier registered with Aim Earn. Sent as X-Aimearn-Key-Id. */\n keyId: string;\n /**\n * Cleartext webhook secret returned by the Aim Earn admin\n * `rotateBrandWebhookSecret` mutation. Used to sign requests and\n * verify response signatures. NEVER log this.\n */\n secret: string;\n /**\n * Full base URL, e.g. `https://webhooks.aimearn.platform.com` or the\n * sandbox auto-generated `https://abc123.execute-api.region.amazonaws.com`.\n */\n endpoint: string;\n /** Optional fetch override for testing / non-Node runtimes. Defaults to global fetch. */\n fetch?: typeof globalThis.fetch;\n retry?: RetryConfig;\n}\n\nconst ROUTE = \"/api/webhooks/order\";\n\nexport class AimearnClient {\n private readonly config: Required<\n Pick<AimearnClientConfig, \"keyId\" | \"secret\" | \"endpoint\">\n > & {\n fetch: typeof globalThis.fetch;\n retry: ReturnType<typeof resolveRetryConfig>;\n };\n\n constructor(config: AimearnClientConfig) {\n if (!config.keyId) throw new Error(\"AimearnClient: keyId is required.\");\n if (!config.secret) throw new Error(\"AimearnClient: secret is required.\");\n if (!config.endpoint) throw new Error(\"AimearnClient: endpoint is required.\");\n\n this.config = {\n keyId: config.keyId,\n secret: config.secret,\n endpoint: config.endpoint.replace(/\\/$/, \"\"),\n fetch: config.fetch ?? globalThis.fetch.bind(globalThis),\n retry: resolveRetryConfig(config.retry),\n };\n }\n\n orderCompleted(input: OrderCompletedInput): Promise<AcceptedResponse> {\n return this.send(\"order.completed\", input);\n }\n\n orderShipped(input: OrderShippedInput): Promise<AcceptedResponse> {\n return this.send(\"order.shipped\", input);\n }\n\n orderRefunded(input: OrderRefundedInput): Promise<AcceptedResponse> {\n return this.send(\"order.refunded\", input);\n }\n\n orderCancelled(input: OrderCancelledInput): Promise<AcceptedResponse> {\n return this.send(\"order.cancelled\", input);\n }\n\n private async send<TData>(\n type: WireEventType,\n input: {\n orderId: string;\n eventId?: string;\n occurredAt?: string;\n data: TData;\n metadata?: Record<string, unknown> | null;\n },\n ): Promise<AcceptedResponse> {\n const envelope: Envelope<TData> = {\n eventId: input.eventId ?? generateEventId(),\n type,\n occurredAt: input.occurredAt ?? new Date().toISOString(),\n orderId: input.orderId,\n data: input.data,\n metadata: input.metadata ?? null,\n };\n const rawBody = JSON.stringify(envelope);\n\n const url = `${this.config.endpoint}${ROUTE}`;\n\n return this.executeWithRetry(url, rawBody);\n }\n\n private async executeWithRetry(\n url: string,\n rawBody: string,\n ): Promise<AcceptedResponse> {\n const { maxAttempts, sleep } = this.config.retry;\n let lastError: AimearnError | undefined;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n const delayMs = backoffDelayMs(attempt, this.config.retry);\n if (delayMs > 0) await sleep(delayMs);\n\n try {\n return await this.attemptOnce(url, rawBody);\n } catch (err) {\n if (!(err instanceof AimearnError)) throw err;\n lastError = err;\n\n if (!isRetriable(err.status)) {\n throw err;\n }\n // Retriable — fall through to next iteration.\n }\n }\n\n throw (\n lastError ??\n new AimearnPlatformError(\n \"internal_error\",\n \"AimearnClient retry budget exhausted with no error captured.\",\n 500,\n )\n );\n }\n\n private async attemptOnce(\n url: string,\n rawBody: string,\n ): Promise<AcceptedResponse> {\n const sigHeader = signRequest(this.config.secret, rawBody);\n\n let response: Response;\n try {\n response = await this.config.fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Aimearn-Key-Id\": this.config.keyId,\n \"X-Aimearn-Signature\": sigHeader,\n },\n body: rawBody,\n });\n } catch (err) {\n // Network failure — retriable per spec §10.1. Wrap as\n // AimearnPlatformError(status:0) so the retry loop catches it.\n throw new AimearnPlatformError(\n \"network_error\",\n `Network error during webhook POST: ${(err as Error).message ?? err}`,\n 0,\n );\n }\n\n const responseBody = await response.text();\n\n // Verify response signature on 2xx — required per spec §5.5 brand\n // contract. Missing/invalid signature on a 2xx is a fatal trust\n // boundary violation.\n if (response.status >= 200 && response.status < 300) {\n const sigOk = verifyResponseSignature(\n this.config.secret,\n response.headers.get(\"x-aimearn-response-signature\"),\n responseBody,\n );\n if (!sigOk) {\n throw new AimearnPlatformError(\n \"response_signature_failed\",\n `Aim Earn response signature missing or invalid (HTTP ${response.status}).`,\n response.status,\n responseBody,\n );\n }\n try {\n return JSON.parse(responseBody) as AcceptedResponse;\n } catch {\n throw new AimearnPlatformError(\n \"internal_error\",\n \"Aim Earn returned 2xx with an unparseable JSON body.\",\n response.status,\n responseBody,\n );\n }\n }\n\n // 4xx / 5xx — parse the error body if possible and throw the\n // typed error class.\n let parsed: unknown = responseBody;\n try {\n parsed = JSON.parse(responseBody);\n } catch {\n // Leave parsed as the raw string.\n }\n throw errorFromResponse(response.status, parsed);\n }\n}\n","// @aimearn/webhook-sdk public surface (server entry).\n//\n// Spec: docs/superpowers/specs/2026-04-29-webhook-ingest-design.md\n// Docs: /docs/sdk in the host app\n\nexport const SDK_VERSION = \"0.1.0\";\n\nexport { AimearnClient } from \"./client\";\nexport type { AimearnClientConfig } from \"./client\";\n\nexport { generateEventId } from \"./eventId\";\nexport {\n signRequest,\n verifyResponseSignature,\n parseSignatureHeader,\n REPLAY_WINDOW_SECONDS,\n} from \"./signing\";\n\nexport {\n AimearnError,\n AimearnAuthError,\n AimearnValidationError,\n AimearnNotFoundError,\n AimearnForbiddenError,\n AimearnConflictError,\n AimearnPlatformError,\n errorFromResponse,\n} from \"./errors\";\nexport type { AimearnErrorCode } from \"./errors\";\n\nexport {\n isRetriable,\n backoffDelayMs,\n resolveRetryConfig,\n} from \"./retry\";\nexport type { RetryConfig } from \"./retry\";\n\nexport type {\n WireEventType,\n CompletedItem,\n CustomerInfo,\n CompletedData,\n ShippedData,\n RefundedData,\n CancelledData,\n Envelope,\n OrderCompletedInput,\n OrderShippedInput,\n OrderRefundedInput,\n OrderCancelledInput,\n AcceptedResponse,\n} from \"./types\";\n"],"mappings":";AAaA,SAAS,mBAAmB;AAE5B,IAAM,WAAW;AACjB,IAAM,eAAe,SAAS;AAC9B,IAAM,WAAW;AACjB,IAAM,aAAa;AAEnB,SAAS,WAAW,KAAa,KAAqB;AACpD,MAAI,MAAM;AACV,MAAI,IAAI;AACR,WAAS,IAAI,MAAM,GAAG,KAAK,GAAG,KAAK;AACjC,UAAM,MAAM,IAAI;AAChB,UAAM,SAAS,GAAG,IAAI;AACtB,SAAK,IAAI,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,KAAqB;AAGzC,QAAM,QAAQ,YAAY,GAAG;AAC7B,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,WAAO,SAAS,MAAM,CAAC,IAAI,EAAE;AAAA,EAC/B;AACA,SAAO;AACT;AAOO,SAAS,gBAAgB,MAAc,KAAK,IAAI,GAAW;AAChE,SAAO,OAAO,WAAW,KAAK,QAAQ,CAAC,GAAG,aAAa,UAAU,CAAC;AACpE;;;ACpCA,SAAS,YAAY,uBAAuB;AAErC,IAAM,wBAAwB;AAO9B,SAAS,YACd,QACA,SACA,MAAc,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GAClC;AACR,QAAM,KAAK,WAAW,UAAU,MAAM,EACnC,OAAO,GAAG,GAAG,IAAI,OAAO,EAAE,EAC1B,OAAO,KAAK;AACf,SAAO,KAAK,GAAG,OAAO,EAAE;AAC1B;AAWO,SAAS,qBACd,QACwB;AACxB,MAAI,CAAC,OAAQ,QAAO;AAEpB,MAAI,IAAmB;AACvB,MAAI,KAAoB;AACxB,aAAW,QAAQ,OAAO,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAI,KAAK,EAAG,QAAO;AACnB,UAAM,IAAI,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;AACjC,UAAM,IAAI,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK;AAClC,QAAI,MAAM,KAAK;AACb,YAAM,SAAS,OAAO,CAAC;AACvB,UAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG,QAAO;AACpD,UAAI;AAAA,IACN,WAAW,MAAM,MAAM;AAErB,UAAI,CAAC,iBAAiB,KAAK,CAAC,EAAG,QAAO;AACtC,WAAK;AAAA,IACP;AAAA,EACF;AACA,MAAI,MAAM,QAAQ,OAAO,KAAM,QAAO;AACtC,SAAO,EAAE,GAAG,GAAG;AACjB;AASO,SAAS,wBACd,QACA,aACA,cACA,aAAqB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GACxC;AACT,QAAM,MAAM,qBAAqB,WAAW;AAC5C,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,KAAK,IAAI,aAAa,IAAI,CAAC,IAAI,sBAAuB,QAAO;AAEjE,QAAM,WAAW,WAAW,UAAU,MAAM,EACzC,OAAO,GAAG,IAAI,CAAC,IAAI,YAAY,EAAE,EACjC,OAAO,KAAK;AAEf,MAAI,SAAS,WAAW,IAAI,GAAG,OAAQ,QAAO;AAC9C,SAAO,gBAAgB,OAAO,KAAK,QAAQ,GAAG,OAAO,KAAK,IAAI,EAAE,CAAC;AACnE;;;AC5DO,IAAM,eAAN,cAA2B,MAAM;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EAET,YACE,MACA,SACA,QACA,cACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,eAAe;AAAA,EACtB;AACF;AAGO,IAAM,mBAAN,cAA+B,aAAa;AAAA,EACjD,YAAY,MAAwB,SAAiB,SAAS,KAAK,cAAwB;AACzF,UAAM,MAAM,SAAS,QAAQ,YAAY;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAGO,IAAM,yBAAN,cAAqC,aAAa;AAAA,EACvD,YAAY,MAAwB,SAAiB,SAAS,KAAK,cAAwB;AACzF,UAAM,MAAM,SAAS,QAAQ,YAAY;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAGO,IAAM,uBAAN,cAAmC,aAAa;AAAA,EACrD,YAAY,MAAwB,SAAiB,SAAS,KAAK,cAAwB;AACzF,UAAM,MAAM,SAAS,QAAQ,YAAY;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAGO,IAAM,wBAAN,cAAoC,aAAa;AAAA,EACtD,YAAY,MAAwB,SAAiB,SAAS,KAAK,cAAwB;AACzF,UAAM,MAAM,SAAS,QAAQ,YAAY;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAGO,IAAM,uBAAN,cAAmC,aAAa;AAAA,EACrD,YAAY,MAAwB,SAAiB,SAAS,KAAK,cAAwB;AACzF,UAAM,MAAM,SAAS,QAAQ,YAAY;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAGO,IAAM,uBAAN,cAAmC,aAAa;AAAA,EACrD,YAAY,MAAwB,SAAiB,SAAS,KAAK,cAAwB;AACzF,UAAM,MAAM,SAAS,QAAQ,YAAY;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAM,uBAA4D;AAAA,EAChE,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAOO,SAAS,kBACd,QACA,MACc;AACd,QAAM,YACJ,OAAO,SAAS,YAChB,SAAS,QACT,WAAW,QACX,OAAQ,KAA4B,UAAU,WACxC,KAAqC,QACvC;AAEN,QAAM,OACJ,qBAAqB,MAAM,MAC1B,UAAU,MAAM,uBAAuB;AAE1C,SAAO,IAAI;AAAA,IACT;AAAA,IACA,YAAY,MAAM,KAAK,SAAS;AAAA,IAChC;AAAA,IACA;AAAA,EACF;AACF;;;AC/GA,IAAM,WAAkC;AAAA,EACtC,aAAa;AAAA,EACb,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,OAAO,CAAC,OAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACjE;AAEO,SAAS,mBACd,QACuB;AACvB,SAAO,EAAE,GAAG,UAAU,GAAI,UAAU,CAAC,EAAG;AAC1C;AAUO,SAAS,YAAY,QAAyB;AACnD,SAAO,WAAW,KAAK,WAAW,OAAO,UAAU;AACrD;AAMO,SAAS,eACd,SACA,QACQ;AAGR,MAAI,WAAW,EAAG,QAAO;AACzB,QAAM,MAAM,OAAO,cAAc,MAAM,UAAU;AACjD,SAAO,KAAK,IAAI,KAAK,OAAO,UAAU;AACxC;;;ACHA,IAAM,QAAQ;AAEP,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EAOjB,YAAY,QAA6B;AACvC,QAAI,CAAC,OAAO,MAAO,OAAM,IAAI,MAAM,mCAAmC;AACtE,QAAI,CAAC,OAAO,OAAQ,OAAM,IAAI,MAAM,oCAAoC;AACxE,QAAI,CAAC,OAAO,SAAU,OAAM,IAAI,MAAM,sCAAsC;AAE5E,SAAK,SAAS;AAAA,MACZ,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO,SAAS,QAAQ,OAAO,EAAE;AAAA,MAC3C,OAAO,OAAO,SAAS,WAAW,MAAM,KAAK,UAAU;AAAA,MACvD,OAAO,mBAAmB,OAAO,KAAK;AAAA,IACxC;AAAA,EACF;AAAA,EAEA,eAAe,OAAuD;AACpE,WAAO,KAAK,KAAK,mBAAmB,KAAK;AAAA,EAC3C;AAAA,EAEA,aAAa,OAAqD;AAChE,WAAO,KAAK,KAAK,iBAAiB,KAAK;AAAA,EACzC;AAAA,EAEA,cAAc,OAAsD;AAClE,WAAO,KAAK,KAAK,kBAAkB,KAAK;AAAA,EAC1C;AAAA,EAEA,eAAe,OAAuD;AACpE,WAAO,KAAK,KAAK,mBAAmB,KAAK;AAAA,EAC3C;AAAA,EAEA,MAAc,KACZ,MACA,OAO2B;AAC3B,UAAM,WAA4B;AAAA,MAChC,SAAS,MAAM,WAAW,gBAAgB;AAAA,MAC1C;AAAA,MACA,YAAY,MAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAAA,MACvD,SAAS,MAAM;AAAA,MACf,MAAM,MAAM;AAAA,MACZ,UAAU,MAAM,YAAY;AAAA,IAC9B;AACA,UAAM,UAAU,KAAK,UAAU,QAAQ;AAEvC,UAAM,MAAM,GAAG,KAAK,OAAO,QAAQ,GAAG,KAAK;AAE3C,WAAO,KAAK,iBAAiB,KAAK,OAAO;AAAA,EAC3C;AAAA,EAEA,MAAc,iBACZ,KACA,SAC2B;AAC3B,UAAM,EAAE,aAAa,MAAM,IAAI,KAAK,OAAO;AAC3C,QAAI;AAEJ,aAAS,UAAU,GAAG,WAAW,aAAa,WAAW;AACvD,YAAM,UAAU,eAAe,SAAS,KAAK,OAAO,KAAK;AACzD,UAAI,UAAU,EAAG,OAAM,MAAM,OAAO;AAEpC,UAAI;AACF,eAAO,MAAM,KAAK,YAAY,KAAK,OAAO;AAAA,MAC5C,SAAS,KAAK;AACZ,YAAI,EAAE,eAAe,cAAe,OAAM;AAC1C,oBAAY;AAEZ,YAAI,CAAC,YAAY,IAAI,MAAM,GAAG;AAC5B,gBAAM;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,UACE,aACA,IAAI;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EAEJ;AAAA,EAEA,MAAc,YACZ,KACA,SAC2B;AAC3B,UAAM,YAAY,YAAY,KAAK,OAAO,QAAQ,OAAO;AAEzD,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,KAAK,OAAO,MAAM,KAAK;AAAA,QACtC,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,oBAAoB,KAAK,OAAO;AAAA,UAChC,uBAAuB;AAAA,QACzB;AAAA,QACA,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,KAAK;AAGZ,YAAM,IAAI;AAAA,QACR;AAAA,QACA,sCAAuC,IAAc,WAAW,GAAG;AAAA,QACnE;AAAA,MACF;AAAA,IACF;AAEA,UAAM,eAAe,MAAM,SAAS,KAAK;AAKzC,QAAI,SAAS,UAAU,OAAO,SAAS,SAAS,KAAK;AACnD,YAAM,QAAQ;AAAA,QACZ,KAAK,OAAO;AAAA,QACZ,SAAS,QAAQ,IAAI,8BAA8B;AAAA,QACnD;AAAA,MACF;AACA,UAAI,CAAC,OAAO;AACV,cAAM,IAAI;AAAA,UACR;AAAA,UACA,wDAAwD,SAAS,MAAM;AAAA,UACvE,SAAS;AAAA,UACT;AAAA,QACF;AAAA,MACF;AACA,UAAI;AACF,eAAO,KAAK,MAAM,YAAY;AAAA,MAChC,QAAQ;AACN,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAIA,QAAI,SAAkB;AACtB,QAAI;AACF,eAAS,KAAK,MAAM,YAAY;AAAA,IAClC,QAAQ;AAAA,IAER;AACA,UAAM,kBAAkB,SAAS,QAAQ,MAAM;AAAA,EACjD;AACF;;;AC7NO,IAAM,cAAc;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/eventId.ts","../src/signing.ts","../src/errors.ts","../src/retry.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["// Stripe-style ULID-shaped idempotency key, formatted as\n// `evt_<26-char-base32-crockford>`. Same external shape as our backend\n// uses, but generated client-side so brands can collapse retries from\n// the same call site naturally — re-issuing the same `eventId` against\n// the platform is idempotent (the ingest Lambda's\n// `attribute_not_exists(orderId)` guard catches duplicates).\n//\n// Deliberately not pulling in the `ulid` npm package — keeping the\n// SDK dependency-free at runtime keeps the install size small and\n// avoids supply-chain surface. The implementation below is a faithful\n// 48-bit-time + 80-bit-randomness ULID with Crockford base32, matching\n// the spec at https://github.com/ulid/spec.\n\nimport { randomBytes } from \"node:crypto\";\n\nconst ENCODING = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\"; // Crockford base32\nconst ENCODING_LEN = ENCODING.length;\nconst TIME_LEN = 10;\nconst RANDOM_LEN = 16;\n\nfunction encodeTime(now: number, len: number): string {\n let out = \"\";\n let n = now;\n for (let i = len - 1; i >= 0; i--) {\n const mod = n % ENCODING_LEN;\n out = ENCODING[mod] + out;\n n = (n - mod) / ENCODING_LEN;\n }\n return out;\n}\n\nfunction encodeRandom(len: number): string {\n // 1 byte = 256 values; we map each to one of 32 base32 chars by\n // taking the low 5 bits. Equivalent randomness density.\n const bytes = randomBytes(len);\n let out = \"\";\n for (let i = 0; i < len; i++) {\n out += ENCODING[bytes[i] & 31];\n }\n return out;\n}\n\n/**\n * Returns a fresh idempotency key of shape `evt_<ULID>`.\n * 48-bit timestamp (millisecond precision) + 80-bit randomness.\n * Sortable lexicographically by creation time.\n */\nexport function generateEventId(now: number = Date.now()): string {\n return `evt_${encodeTime(now, TIME_LEN)}${encodeRandom(RANDOM_LEN)}`;\n}\n","// HMAC-SHA256 signing + verification for the Aim Earn webhook contract.\n// Spec: 2026-04-29-webhook-ingest-design.md §5.\n//\n// Request:\n// X-Aimearn-Signature: t=<unix>,v1=<hex>\n// v1 = HMAC-SHA256(secret, `${t}.${rawBody}`)\n//\n// Response:\n// X-Aimearn-Response-Signature: same shape; same per-brand secret.\n//\n// Brand SDK side: we sign every outbound request and verify every\n// inbound response. Replay window 300s.\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nexport const REPLAY_WINDOW_SECONDS = 300;\n\n/**\n * Builds the `X-Aimearn-Signature` header value for a request body.\n * Caller supplies the raw body bytes — never re-serialize after\n * signing, or the platform's HMAC verify will fail.\n */\nexport function signRequest(\n secret: string,\n rawBody: string,\n now: number = Math.floor(Date.now() / 1000),\n): string {\n const v1 = createHmac(\"sha256\", secret)\n .update(`${now}.${rawBody}`)\n .digest(\"hex\");\n return `t=${now},v1=${v1}`;\n}\n\nexport interface ParsedSignature {\n t: number;\n v1: string;\n}\n\n/**\n * Parses a `t=<unix>,v1=<hex>` header. Returns null on any malformed\n * input — caller treats null as \"missing/unverifiable signature.\"\n */\nexport function parseSignatureHeader(\n header: string | undefined | null,\n): ParsedSignature | null {\n if (!header) return null;\n\n let t: number | null = null;\n let v1: string | null = null;\n for (const part of header.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) return null;\n const k = part.slice(0, eq).trim();\n const v = part.slice(eq + 1).trim();\n if (k === \"t\") {\n const parsed = Number(v);\n if (!Number.isFinite(parsed) || parsed <= 0) return null;\n t = parsed;\n } else if (k === \"v1\") {\n // 64 lowercase hex chars for SHA-256.\n if (!/^[0-9a-f]{64}$/.test(v)) return null;\n v1 = v;\n }\n }\n if (t === null || v1 === null) return null;\n return { t, v1 };\n}\n\n/**\n * Verifies the platform's response signature against the response\n * body bytes. Returns true on match, false on any failure (missing\n * header, stale timestamp, mismatched HMAC). Brand SDK must reject\n * 2xx responses without a verifiable signature; 4xx/5xx may be\n * accepted as platform-level errors per spec §5.5.\n */\nexport function verifyResponseSignature(\n secret: string,\n headerValue: string | undefined | null,\n responseBody: string,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): boolean {\n const sig = parseSignatureHeader(headerValue);\n if (!sig) return false;\n if (Math.abs(nowSeconds - sig.t) > REPLAY_WINDOW_SECONDS) return false;\n\n const expected = createHmac(\"sha256\", secret)\n .update(`${sig.t}.${responseBody}`)\n .digest(\"hex\");\n\n if (expected.length !== sig.v1.length) return false;\n return timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));\n}\n","// Typed error hierarchy mapping platform `WebhookDelivery.result`\n// values onto SDK-side error classes. Brand error handlers can switch\n// on `error.code` (the wire-format result string) or use `instanceof`.\n//\n// Spec: 2026-04-29-webhook-ingest-design.md §10.1 retry contract.\n\nexport type AimearnErrorCode =\n | \"signature_failed\"\n | \"parse_error\"\n | \"duplicate\"\n | \"country_not_published\"\n | \"product_not_found\"\n | \"unknown_referral\"\n | \"discarded\"\n | \"brand_not_found\"\n | \"brand_disabled\"\n | \"missing_key_id\"\n | \"rate_limited\"\n | \"body_too_large\"\n | \"unknown_event_type\"\n | \"unknown_order\"\n | \"brand_mismatch\"\n | \"cancelled_after_shipment\"\n | \"invalid_item_index\"\n | \"subtotal_mismatch\"\n | \"too_many_items\"\n // SDK-side codes for failure modes that don't reach the platform:\n | \"network_error\"\n | \"response_signature_failed\"\n | \"internal_error\";\n\nexport class AimearnError extends Error {\n readonly code: AimearnErrorCode;\n readonly status: number;\n readonly responseBody?: unknown;\n\n constructor(\n code: AimearnErrorCode,\n message: string,\n status: number,\n responseBody?: unknown,\n ) {\n super(message);\n this.name = \"AimearnError\";\n this.code = code;\n this.status = status;\n this.responseBody = responseBody;\n }\n}\n\n/** 401 — signature/auth failure. Brand should fix secret/clock; do NOT retry. */\nexport class AimearnAuthError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 401, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnAuthError\";\n }\n}\n\n/** 400 — payload schema violation. Brand-side bug; do NOT retry. */\nexport class AimearnValidationError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 400, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnValidationError\";\n }\n}\n\n/** 404 — referenced order or brand not found. Brand should re-check IDs; do NOT retry. */\nexport class AimearnNotFoundError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 404, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnNotFoundError\";\n }\n}\n\n/** 403 — brand disabled / brand-mismatch. Escalate to platform admin. */\nexport class AimearnForbiddenError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 403, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnForbiddenError\";\n }\n}\n\n/** 409 — state-machine violation (e.g. order.cancelled after shipment). */\nexport class AimearnConflictError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 409, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnConflictError\";\n }\n}\n\n/** 5xx / network — platform-side error. SDK retries automatically up to the configured limit. */\nexport class AimearnPlatformError extends AimearnError {\n constructor(code: AimearnErrorCode, message: string, status = 500, responseBody?: unknown) {\n super(code, message, status, responseBody);\n this.name = \"AimearnPlatformError\";\n }\n}\n\nconst ERROR_CTOR_BY_STATUS: Record<number, typeof AimearnError> = {\n 400: AimearnValidationError,\n 401: AimearnAuthError,\n 403: AimearnForbiddenError,\n 404: AimearnNotFoundError,\n 409: AimearnConflictError,\n 413: AimearnValidationError,\n 429: AimearnPlatformError,\n};\n\n/**\n * Converts an HTTP error response into the right SDK error class.\n * `code` is the platform's `error` field from the response body; falls\n * back to `internal_error` if the body shape is unexpected.\n */\nexport function errorFromResponse(\n status: number,\n body: unknown,\n): AimearnError {\n const errorCode =\n typeof body === \"object\" &&\n body !== null &&\n \"error\" in body &&\n typeof (body as { error: unknown }).error === \"string\"\n ? ((body as { error: AimearnErrorCode }).error)\n : \"internal_error\";\n\n const Ctor =\n ERROR_CTOR_BY_STATUS[status] ??\n (status >= 500 ? AimearnPlatformError : AimearnError);\n\n return new Ctor(\n errorCode,\n `Aim Earn ${status}: ${errorCode}`,\n status,\n body,\n );\n}\n","// Exponential-backoff retry policy for the SDK's outbound POSTs.\n// Spec: 2026-04-29-webhook-ingest-design.md §10.1.\n//\n// 2xx → return result\n// 4xx (except 429) → throw immediately, no retry\n// 429 / 5xx / network → retry with exponential backoff\n//\n// Default: 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, … up to 24 attempts ≈ 24h.\n\nexport interface RetryConfig {\n /** Maximum number of total attempts (including the first). Default 24. */\n maxAttempts?: number;\n /** Base delay in milliseconds. Default 1000. */\n baseDelayMs?: number;\n /** Cap individual delay; default 60_000ms. */\n maxDelayMs?: number;\n /**\n * Hook for tests / cancellation. Receives the milliseconds the SDK\n * intends to sleep; should resolve after that delay (or reject to\n * abort the retry loop).\n */\n sleep?: (ms: number) => Promise<void>;\n}\n\nconst DEFAULTS: Required<RetryConfig> = {\n maxAttempts: 24,\n baseDelayMs: 1000,\n maxDelayMs: 60_000,\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n};\n\nexport function resolveRetryConfig(\n config?: RetryConfig,\n): Required<RetryConfig> {\n return { ...DEFAULTS, ...(config ?? {}) };\n}\n\n/**\n * Returns true if the platform's HTTP status (or the SDK's network-error\n * sentinel of 0) should trigger a retry.\n *\n * - 0 — fetch threw; treat as retriable network failure\n * - 429 — explicit rate-limit; back off and retry\n * - 5xx — platform-side error; retry per spec §10.1\n */\nexport function isRetriable(status: number): boolean {\n return status === 0 || status === 429 || status >= 500;\n}\n\n/**\n * Pure delay-curve calculator. Attempt number is 1-indexed (the\n * first retry — i.e. the 2nd total attempt — uses attempt=2).\n */\nexport function backoffDelayMs(\n attempt: number,\n config: Required<RetryConfig>,\n): number {\n // Exponential: base * 2^(attempt - 1). attempt=1 → no delay (initial\n // call); attempt=2 → base; attempt=3 → 2*base; ...\n if (attempt <= 1) return 0;\n const raw = config.baseDelayMs * 2 ** (attempt - 2);\n return Math.min(raw, config.maxDelayMs);\n}\n","// Aim Earn webhook client. Brand-facing API surface:\n//\n// const client = new AimearnClient({ keyId, secret, endpoint });\n// await client.orderCompleted({ orderId, data: {...} });\n// await client.orderShipped({ orderId, data: {...} });\n// await client.orderRefunded({ orderId, data: {...} });\n// await client.orderCancelled({ orderId, data: {...} });\n//\n// Each method:\n// 1. Builds the envelope (auto-generates eventId via ULID if omitted)\n// 2. Serializes once to a stable string (the same bytes get signed)\n// 3. Signs with HMAC-SHA256 over `${t}.${rawBody}` per spec §5\n// 4. POSTs with X-Aimearn-Key-Id + X-Aimearn-Signature\n// 5. Retries 5xx/429 with exponential backoff up to maxAttempts\n// 6. Verifies the X-Aimearn-Response-Signature on 2xx (throws if missing/invalid)\n// 7. Returns the parsed AcceptedResponse, or throws a typed AimearnError\n\nimport { generateEventId } from \"./eventId\";\nimport { signRequest, verifyResponseSignature } from \"./signing\";\nimport {\n AimearnError,\n AimearnPlatformError,\n errorFromResponse,\n} from \"./errors\";\nimport {\n backoffDelayMs,\n isRetriable,\n resolveRetryConfig,\n type RetryConfig,\n} from \"./retry\";\nimport type {\n AcceptedResponse,\n Envelope,\n OrderCancelledInput,\n OrderCompletedInput,\n OrderRefundedInput,\n OrderShippedInput,\n WireEventType,\n} from \"./types\";\n\nexport interface AimearnClientConfig {\n /** Brand identifier registered with Aim Earn. Sent as X-Aimearn-Key-Id. */\n keyId: string;\n /**\n * Cleartext webhook secret returned by the Aim Earn admin\n * `rotateBrandWebhookSecret` mutation. Used to sign requests and\n * verify response signatures. NEVER log this.\n */\n secret: string;\n /**\n * Full base URL, e.g. `https://webhooks.aimearn.platform.com` or the\n * sandbox auto-generated `https://abc123.execute-api.region.amazonaws.com`.\n */\n endpoint: string;\n /** Optional fetch override for testing / non-Node runtimes. Defaults to global fetch. */\n fetch?: typeof globalThis.fetch;\n retry?: RetryConfig;\n}\n\nconst ROUTE = \"/api/webhooks/order\";\n\nexport class AimearnClient {\n private readonly config: Required<\n Pick<AimearnClientConfig, \"keyId\" | \"secret\" | \"endpoint\">\n > & {\n fetch: typeof globalThis.fetch;\n retry: ReturnType<typeof resolveRetryConfig>;\n };\n\n constructor(config: AimearnClientConfig) {\n if (!config.keyId) throw new Error(\"AimearnClient: keyId is required.\");\n if (!config.secret) throw new Error(\"AimearnClient: secret is required.\");\n if (!config.endpoint) throw new Error(\"AimearnClient: endpoint is required.\");\n\n this.config = {\n keyId: config.keyId,\n secret: config.secret,\n endpoint: config.endpoint.replace(/\\/$/, \"\"),\n fetch: config.fetch ?? globalThis.fetch.bind(globalThis),\n retry: resolveRetryConfig(config.retry),\n };\n }\n\n orderCompleted(input: OrderCompletedInput): Promise<AcceptedResponse> {\n return this.send(\"order.completed\", input);\n }\n\n orderShipped(input: OrderShippedInput): Promise<AcceptedResponse> {\n return this.send(\"order.shipped\", input);\n }\n\n orderRefunded(input: OrderRefundedInput): Promise<AcceptedResponse> {\n return this.send(\"order.refunded\", input);\n }\n\n orderCancelled(input: OrderCancelledInput): Promise<AcceptedResponse> {\n return this.send(\"order.cancelled\", input);\n }\n\n private async send<TData>(\n type: WireEventType,\n input: {\n orderId: string;\n eventId?: string;\n occurredAt?: string;\n data: TData;\n metadata?: Record<string, unknown> | null;\n },\n ): Promise<AcceptedResponse> {\n const envelope: Envelope<TData> = {\n eventId: input.eventId ?? generateEventId(),\n type,\n occurredAt: input.occurredAt ?? new Date().toISOString(),\n orderId: input.orderId,\n data: input.data,\n metadata: input.metadata ?? null,\n };\n const rawBody = JSON.stringify(envelope);\n\n const url = `${this.config.endpoint}${ROUTE}`;\n\n return this.executeWithRetry(url, rawBody);\n }\n\n private async executeWithRetry(\n url: string,\n rawBody: string,\n ): Promise<AcceptedResponse> {\n const { maxAttempts, sleep } = this.config.retry;\n let lastError: AimearnError | undefined;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n const delayMs = backoffDelayMs(attempt, this.config.retry);\n if (delayMs > 0) await sleep(delayMs);\n\n try {\n return await this.attemptOnce(url, rawBody);\n } catch (err) {\n if (!(err instanceof AimearnError)) throw err;\n lastError = err;\n\n if (!isRetriable(err.status)) {\n throw err;\n }\n // Retriable — fall through to next iteration.\n }\n }\n\n throw (\n lastError ??\n new AimearnPlatformError(\n \"internal_error\",\n \"AimearnClient retry budget exhausted with no error captured.\",\n 500,\n )\n );\n }\n\n private async attemptOnce(\n url: string,\n rawBody: string,\n ): Promise<AcceptedResponse> {\n const sigHeader = signRequest(this.config.secret, rawBody);\n\n let response: Response;\n try {\n response = await this.config.fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Aimearn-Key-Id\": this.config.keyId,\n \"X-Aimearn-Signature\": sigHeader,\n },\n body: rawBody,\n });\n } catch (err) {\n // Network failure — retriable per spec §10.1. Wrap as\n // AimearnPlatformError(status:0) so the retry loop catches it.\n throw new AimearnPlatformError(\n \"network_error\",\n `Network error during webhook POST: ${(err as Error).message ?? err}`,\n 0,\n );\n }\n\n const responseBody = await response.text();\n\n // Verify response signature on 2xx — required per spec §5.5 brand\n // contract. Missing/invalid signature on a 2xx is a fatal trust\n // boundary violation.\n if (response.status >= 200 && response.status < 300) {\n const sigOk = verifyResponseSignature(\n this.config.secret,\n response.headers.get(\"x-aimearn-response-signature\"),\n responseBody,\n );\n if (!sigOk) {\n throw new AimearnPlatformError(\n \"response_signature_failed\",\n `Aim Earn response signature missing or invalid (HTTP ${response.status}).`,\n response.status,\n responseBody,\n );\n }\n try {\n return JSON.parse(responseBody) as AcceptedResponse;\n } catch {\n throw new AimearnPlatformError(\n \"internal_error\",\n \"Aim Earn returned 2xx with an unparseable JSON body.\",\n response.status,\n responseBody,\n );\n }\n }\n\n // 4xx / 5xx — parse the error body if possible and throw the\n // typed error class.\n let parsed: unknown = responseBody;\n try {\n parsed = JSON.parse(responseBody);\n } catch {\n // Leave parsed as the raw string.\n }\n throw errorFromResponse(response.status, parsed);\n }\n}\n","// @aimearn/webhook-sdk public surface (server entry).\n//\n// Spec: docs/superpowers/specs/2026-04-29-webhook-ingest-design.md\n// Docs: /docs/sdk in the host app\n\nexport const SDK_VERSION = \"1.0.0\";\n\nexport { AimearnClient } from \"./client\";\nexport type { AimearnClientConfig } from \"./client\";\n\nexport { generateEventId } from \"./eventId\";\nexport {\n signRequest,\n verifyResponseSignature,\n parseSignatureHeader,\n REPLAY_WINDOW_SECONDS,\n} from \"./signing\";\n\nexport {\n AimearnError,\n AimearnAuthError,\n AimearnValidationError,\n AimearnNotFoundError,\n AimearnForbiddenError,\n AimearnConflictError,\n AimearnPlatformError,\n errorFromResponse,\n} from \"./errors\";\nexport type { AimearnErrorCode } from \"./errors\";\n\nexport { isRetriable, backoffDelayMs, resolveRetryConfig } from \"./retry\";\nexport type { RetryConfig } from \"./retry\";\n\nexport type {\n WireEventType,\n CompletedItem,\n CustomerInfo,\n CompletedData,\n ShippedData,\n RefundedData,\n CancelledData,\n Envelope,\n OrderCompletedInput,\n OrderShippedInput,\n OrderRefundedInput,\n OrderCancelledInput,\n AcceptedResponse,\n} from \"./types\";\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aimearn/webhook-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Brand SDK for the Aim Earn webhook ingest API — HMAC signing, response verification, retries, idempotency.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
"hmac",
|
|
34
34
|
"ecommerce"
|
|
35
35
|
],
|
|
36
|
-
"homepage": "https://github.com/
|
|
36
|
+
"homepage": "https://github.com/aimearn/webhook-sdk#readme",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "git+https://github.com/
|
|
39
|
+
"url": "git+https://github.com/aimearn/webhook-sdk.git",
|
|
40
40
|
"directory": "packages/webhook-sdk"
|
|
41
41
|
},
|
|
42
42
|
"bugs": {
|
|
43
|
-
"url": "https://github.com/
|
|
43
|
+
"url": "https://github.com/aimearn/webhook-sdk/issues"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public",
|