@hello-bill/node 1.0.3 → 1.0.4

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.
@@ -161,8 +161,16 @@ var InMemoryWebhookDedupeStore = class {
161
161
  };
162
162
 
163
163
  // src/webhook/handler.ts
164
+ var UnknownWebhookEventError = class extends Error {
165
+ code = "webhook.unknown_event_type";
166
+ constructor(eventType) {
167
+ super(`Unhandled webhook event type: ${eventType}`);
168
+ this.name = "UnknownWebhookEventError";
169
+ Object.setPrototypeOf(this, new.target.prototype);
170
+ }
171
+ };
164
172
  function assertNever(x) {
165
- throw new Error(`Unhandled webhook variant: ${JSON.stringify(x)}`);
173
+ throw new UnknownWebhookEventError(x.type ?? JSON.stringify(x));
166
174
  }
167
175
  var noopLogger = {
168
176
  debug: () => {
@@ -327,13 +335,17 @@ function createWebhookHandler(opts) {
327
335
  safeLogger.error("hellobill.webhook.handler_error", {
328
336
  event_id: event.id,
329
337
  event_type: event.type,
330
- message: err.message
338
+ message: err.message,
339
+ // Include the typed error code when present so partners can identify
340
+ // known error classes (e.g. unknown event type) in log aggregation.
341
+ ...err != null && typeof err === "object" && "code" in err ? { code: err.code } : {}
331
342
  });
332
343
  });
333
344
  };
334
345
  }
335
346
 
336
347
  exports.InMemoryWebhookDedupeStore = InMemoryWebhookDedupeStore;
348
+ exports.UnknownWebhookEventError = UnknownWebhookEventError;
337
349
  exports.WebhookVerificationError = WebhookVerificationError;
338
350
  exports.createWebhookHandler = createWebhookHandler;
339
351
  exports.verifyWebhook = verifyWebhook;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/webhook/verify.ts","../../src/webhook/dedupe-store.ts","../../src/webhook/handler.ts"],"names":["createHmac","timingSafeEqual"],"mappings":"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","file":"index.js","sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookEvent } from '../types/index.js';\n\nexport interface VerifyWebhookOptions {\n /** Replay window in seconds. Default 300. */\n tolerance?: number;\n /** Signature versions to accept. Default ['v1']. Use ['v1', 'v2'] during key rotation. */\n supportedVersions?: ('v1' | 'v2')[];\n /** Override `Date.now()` for tests. */\n now?: () => number;\n}\n\nexport class WebhookVerificationError extends Error {\n constructor(\n message: string,\n public code: string,\n ) {\n super(message);\n this.name = 'WebhookVerificationError';\n }\n}\n\ninterface ParsedHeader {\n t: number;\n signatures: Record<string, string>;\n}\n\nfunction parseHeader(signatureHeader: string): ParsedHeader {\n if (!signatureHeader || typeof signatureHeader !== 'string') {\n throw new WebhookVerificationError(\n 'Missing signature header',\n 'signature.missing_header',\n );\n }\n\n const parts = signatureHeader.split(',').map((p) => p.trim()).filter(Boolean);\n let t: number | undefined;\n const signatures: Record<string, string> = {};\n\n for (const part of parts) {\n const eq = part.indexOf('=');\n if (eq === -1) {\n throw new WebhookVerificationError(\n 'Malformed signature header element',\n 'signature.malformed',\n );\n }\n const key = part.slice(0, eq);\n const value = part.slice(eq + 1);\n if (key === 't') {\n t = Number(value);\n if (!Number.isFinite(t)) {\n throw new WebhookVerificationError(\n 'Malformed timestamp in signature header',\n 'signature.malformed',\n );\n }\n } else if (key.length > 0 && value.length > 0) {\n signatures[key] = value;\n }\n }\n\n if (t === undefined) {\n throw new WebhookVerificationError(\n 'Missing timestamp in signature header',\n 'signature.malformed',\n );\n }\n if (Object.keys(signatures).length === 0) {\n throw new WebhookVerificationError(\n 'No signature values in header',\n 'signature.malformed',\n );\n }\n\n return { t, signatures };\n}\n\nfunction computeHmacHex(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('hex');\n}\n\nfunction constantTimeHexEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n try {\n return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));\n } catch {\n return false;\n }\n}\n\n/**\n * Verifies a HelloBill webhook delivery.\n * Header format: `t=<unix>,v1=<hex>,v2=<hex>`.\n * HMAC computed over `${t}.${rawBody}` (raw bytes — do not re-serialise).\n */\nexport function verifyWebhook(\n rawBody: Buffer | string,\n signatureHeader: string,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): WebhookEvent {\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const now = opts.now ?? Date.now;\n\n const { t, signatures } = parseHeader(signatureHeader);\n\n const nowSeconds = Math.floor(now() / 1000);\n const skew = Math.abs(nowSeconds - t);\n if (skew > tolerance) {\n throw new WebhookVerificationError(\n `Signature timestamp outside tolerance (${skew}s > ${tolerance}s)`,\n 'signature.expired',\n );\n }\n\n const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);\n if (!hasAnyKnownVersion) {\n throw new WebhookVerificationError(\n 'No supported signature version present in header',\n 'signature.unsupported_version',\n );\n }\n\n const bodyStr =\n typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');\n const signingInput = `${t}.${bodyStr}`;\n\n for (const version of supportedVersions) {\n const provided = signatures[version];\n if (!provided) continue;\n const expected = computeHmacHex(secret, signingInput);\n if (constantTimeHexEqual(expected, provided)) {\n try {\n return JSON.parse(bodyStr) as WebhookEvent;\n } catch {\n throw new WebhookVerificationError(\n 'Verified payload is not valid JSON',\n 'signature.malformed',\n );\n }\n }\n }\n\n throw new WebhookVerificationError(\n 'Signature does not match expected value',\n 'signature.invalid',\n );\n}\n\n/**\n * Verifies a HelloBill webhook delivery without parsing the payload.\n * Returns `true` only if the timestamp is within tolerance AND at least one\n * supported scheme verifies. Returns `false` for every other case (missing/\n * malformed header, expired timestamp, no supported version, signature\n * mismatch). Never throws on verification failure — callers route on the\n * boolean. Re-throws non-verification errors (e.g. unexpected crypto failures).\n *\n * Unlike the throwing `verifyWebhook`, this function does NOT attempt to parse\n * the body as JSON — signature verification is purely over the raw bytes.\n * Callers that need the parsed event should call `verifyWebhook` directly or\n * parse separately after calling this function.\n *\n * @param rawBody Raw request body bytes (NOT re-serialised JSON).\n * @param signatureHeader Value of `X-HelloBill-Signature`.\n * @param secret Partner-issued webhook secret (env `HELLOBILL_WEBHOOK_SECRET`).\n * @param opts Optional `tolerance` (default 300s), `supportedVersions`\n * (default `['v1']`), `now` (test injection).\n */\nexport function verifyWebhookSignature(\n rawBody: Buffer | string,\n signatureHeader: string,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): boolean {\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const now = opts.now ?? Date.now;\n\n let parsed: ParsedHeader;\n try {\n parsed = parseHeader(signatureHeader);\n } catch (err) {\n if (err instanceof WebhookVerificationError) return false;\n throw err;\n }\n\n const { t, signatures } = parsed;\n\n const nowSeconds = Math.floor(now() / 1000);\n const skew = Math.abs(nowSeconds - t);\n if (skew > tolerance) return false;\n\n const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);\n if (!hasAnyKnownVersion) return false;\n\n const bodyStr =\n typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');\n const signingInput = `${t}.${bodyStr}`;\n\n for (const version of supportedVersions) {\n const provided = signatures[version];\n if (!provided) continue;\n const expected = computeHmacHex(secret, signingInput);\n if (constantTimeHexEqual(expected, provided)) {\n return true;\n }\n }\n\n return false;\n}\n","/**\n * Webhook idempotency dedupe store. Keyed on `WebhookEnvelope.id`.\n *\n * Default `InMemoryWebhookDedupeStore` is fine for single-process deployments\n * (and unit tests). Multi-replica deployments MUST inject a shared backend\n * (e.g. Redis SETEX, RDS `INSERT ... ON CONFLICT DO NOTHING`). Per spec §11.1\n * line 2270: \"Use the `id` field to deduplicate.\"\n */\nexport interface WebhookDedupeStore {\n /** Returns true if `id` has been recorded within its TTL. Never throws on miss. */\n has(id: string): Promise<boolean> | boolean;\n /** Records `id` with the given TTL in seconds. Idempotent on repeat calls. */\n record(id: string, ttlSeconds: number): Promise<void> | void;\n}\n\n/** In-memory implementation. NOT durable across process restarts. Tests + single-proc deploys. */\nexport class InMemoryWebhookDedupeStore implements WebhookDedupeStore {\n private readonly store = new Map<string, number>();\n\n // Optional test injection. Defaults to Date.now.\n constructor(private readonly now: () => number = () => Date.now()) {}\n\n async has(id: string): Promise<boolean> {\n const expiresAt = this.store.get(id);\n if (expiresAt === undefined) return false;\n // Inclusive boundary: at exactly expiresAt, treat as expired. Tested via handler test 6.5.\n if (expiresAt <= this.now()) {\n this.store.delete(id);\n return false;\n }\n return true;\n }\n\n async record(id: string, ttlSeconds: number): Promise<void> {\n this.store.set(id, this.now() + ttlSeconds * 1000);\n }\n}\n","import type {\n BankDetailsCollected,\n MoveOutNotificationFailed,\n MoveOutNotificationSent,\n SessionAccountCreated,\n SessionAppDeferred,\n SessionAppInstalled,\n SessionDetailsConfirmed,\n SessionEmbedOpened,\n SessionLoaSigned,\n SessionProductsSelected,\n SetupStatusChanged,\n SubscriptionChanged,\n WebhookEvent,\n} from '../types/index.js';\nimport type { Logger } from '../core/token-manager.js';\nimport { verifyWebhookSignature, type VerifyWebhookOptions } from './verify.js';\nimport {\n InMemoryWebhookDedupeStore,\n type WebhookDedupeStore,\n} from './dedupe-store.js';\nimport type { HellobillRequest, HellobillResponse } from '../core/handler.js';\n\n/** Per-event runtime context passed to every typed handler. */\nexport interface WebhookHandlerContext {\n /**\n * Logger guaranteed to have `info` defined — sourced from `withInfoFallback`\n * at handler construction. Partner handlers may call `ctx.logger.info(...)`\n * without a null-check regardless of what Logger literal was supplied.\n */\n logger: Logger & { info: (msg: string, meta?: Record<string, unknown>) => void };\n headers: Record<string, string | string[] | undefined>;\n deprecation: string | null;\n}\n\n/**\n * Typed handlers — one per `WebhookEvent` variant. All optional; an\n * unprovided variant logs at info (`unhandled_variant`) and the dispatcher\n * still responds 2xx so HelloBill stops retrying.\n *\n * Handlers run async AFTER the route has already responded; throwing\n * from a handler does NOT cause HelloBill to retry (a 2xx was already\n * written). Errors are logged via `opts.logger?.error`.\n */\nexport interface WebhookHandlers {\n onSessionEmbedOpened?(event: SessionEmbedOpened, ctx: WebhookHandlerContext): Promise<void>;\n onSessionDetailsConfirmed?(event: SessionDetailsConfirmed, ctx: WebhookHandlerContext): Promise<void>;\n onSessionProductsSelected?(event: SessionProductsSelected, ctx: WebhookHandlerContext): Promise<void>;\n onSessionLoaSigned?(event: SessionLoaSigned, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAccountCreated?(event: SessionAccountCreated, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAppInstalled?(event: SessionAppInstalled, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAppDeferred?(event: SessionAppDeferred, ctx: WebhookHandlerContext): Promise<void>;\n onSubscriptionChanged?(event: SubscriptionChanged, ctx: WebhookHandlerContext): Promise<void>;\n onSetupStatusChanged?(event: SetupStatusChanged, ctx: WebhookHandlerContext): Promise<void>;\n onMoveOutNotificationSent?(event: MoveOutNotificationSent, ctx: WebhookHandlerContext): Promise<void>;\n onMoveOutNotificationFailed?(event: MoveOutNotificationFailed, ctx: WebhookHandlerContext): Promise<void>;\n onBankDetailsCollected?(event: BankDetailsCollected, ctx: WebhookHandlerContext): Promise<void>;\n}\n\n/** Options for the webhook route. Lives alongside `CreateHellobillHandlerOptions`. */\nexport interface WebhookHandlerOptions {\n /**\n * Partner webhook secret. From `process.env.HELLOBILL_WEBHOOK_SECRET`.\n * MUST be a non-empty string at `createWebhookHandler` call time — the\n * factory throws synchronously on `null`/`undefined`/`''` so that an\n * unset env var is caught immediately rather than silently 401ing every\n * incoming webhook.\n */\n webhookSecret: string;\n /**\n * Optional dedupe-store. Default: in-memory (single-process). Multi-replica\n * deployments MUST supply a shared backend (Redis, RDS, etc.) — see README.\n */\n dedupeStore?: WebhookDedupeStore;\n /**\n * Idempotency window in seconds. Default 86_400 (24h). Per spec §11.1 line 2270.\n */\n dedupeTtlSeconds?: number;\n /** Signature versions accepted. Default `['v1']`. Set to `['v1','v2']` during rotation. */\n supportedVersions?: VerifyWebhookOptions['supportedVersions'];\n /** Timestamp tolerance in seconds. Default 300. */\n tolerance?: number;\n /** Optional logger. Defaults to no-op. */\n logger?: Logger;\n /** Typed handlers — provide as many as you care about; missing ones log \"unhandled\". */\n handlers?: WebhookHandlers;\n /**\n * Test injection for `Date.now()`. Applied to the signature timestamp AND,\n * only when using the default `InMemoryWebhookDedupeStore`, the dedupe TTL\n * clock. Partner-supplied `dedupeStore` instances MUST own their own clock\n * injection — `opts.now` is NOT forwarded to a partner-supplied store.\n */\n now?: () => number;\n}\n\n/** Sentinel — placed on the receiver's `default:` branch to surface compile-time exhaustiveness. */\nfunction assertNever(x: never): never {\n throw new Error(`Unhandled webhook variant: ${JSON.stringify(x)}`);\n}\n\nconst noopLogger: Logger & { info: () => void } = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n};\n\n/**\n * Coalesces a partner-supplied Logger literal that may omit the optional `info`\n * method. Without this, `ctx.logger.info(...)` inside `logUnhandled` raises a\n * runtime TypeError when the partner's Logger only provides debug/warn/error.\n */\nfunction withInfoFallback(\n logger: Logger | undefined,\n): Logger & { info: (msg: string, meta?: Record<string, unknown>) => void } {\n return {\n debug: logger?.debug?.bind(logger) ?? (() => {}),\n info: logger?.info?.bind(logger) ?? (() => {}),\n warn: logger?.warn?.bind(logger) ?? (() => {}),\n error: logger?.error?.bind(logger) ?? (() => {}),\n };\n}\n\nfunction headerValue(\n headers: Record<string, string | string[] | undefined>,\n name: string,\n): string | undefined {\n const lower = name.toLowerCase();\n for (const [k, v] of Object.entries(headers)) {\n if (k.toLowerCase() === lower) return Array.isArray(v) ? v[0] : v;\n }\n return undefined;\n}\n\n/**\n * Dispatch a verified-and-deduped event to its typed handler. Exhaustive over\n * the union. Missing handler slots fall through to `logUnhandled` so a\n * partner-only-cares-about-bank-details deployment never 500s on an\n * unrecognised event type.\n */\nasync function dispatch(\n event: WebhookEvent,\n ctx: WebhookHandlerContext,\n handlers: WebhookHandlers,\n): Promise<void> {\n const logUnhandled = (name: string): void => {\n ctx.logger.info('hellobill.webhook.unhandled_variant', {\n event_id: event.id,\n event_type: event.type,\n handler: name,\n });\n };\n\n switch (event.type) {\n case 'session.embed_opened':\n if (handlers.onSessionEmbedOpened) await handlers.onSessionEmbedOpened(event, ctx);\n else logUnhandled('onSessionEmbedOpened');\n return;\n case 'session.details_confirmed':\n if (handlers.onSessionDetailsConfirmed) await handlers.onSessionDetailsConfirmed(event, ctx);\n else logUnhandled('onSessionDetailsConfirmed');\n return;\n case 'session.products_selected':\n if (handlers.onSessionProductsSelected) await handlers.onSessionProductsSelected(event, ctx);\n else logUnhandled('onSessionProductsSelected');\n return;\n case 'session.loa_signed':\n if (handlers.onSessionLoaSigned) await handlers.onSessionLoaSigned(event, ctx);\n else logUnhandled('onSessionLoaSigned');\n return;\n case 'session.account_created':\n if (handlers.onSessionAccountCreated) await handlers.onSessionAccountCreated(event, ctx);\n else logUnhandled('onSessionAccountCreated');\n return;\n case 'session.app_installed':\n if (handlers.onSessionAppInstalled) await handlers.onSessionAppInstalled(event, ctx);\n else logUnhandled('onSessionAppInstalled');\n return;\n case 'session.app_deferred':\n if (handlers.onSessionAppDeferred) await handlers.onSessionAppDeferred(event, ctx);\n else logUnhandled('onSessionAppDeferred');\n return;\n case 'subscription.changed':\n if (handlers.onSubscriptionChanged) await handlers.onSubscriptionChanged(event, ctx);\n else logUnhandled('onSubscriptionChanged');\n return;\n case 'setup_status.changed':\n if (handlers.onSetupStatusChanged) await handlers.onSetupStatusChanged(event, ctx);\n else logUnhandled('onSetupStatusChanged');\n return;\n case 'move_out_notification.sent':\n if (handlers.onMoveOutNotificationSent) await handlers.onMoveOutNotificationSent(event, ctx);\n else logUnhandled('onMoveOutNotificationSent');\n return;\n case 'move_out_notification.failed':\n if (handlers.onMoveOutNotificationFailed) await handlers.onMoveOutNotificationFailed(event, ctx);\n else logUnhandled('onMoveOutNotificationFailed');\n return;\n case 'bank_details.collected':\n if (handlers.onBankDetailsCollected) await handlers.onBankDetailsCollected(event, ctx);\n else logUnhandled('onBankDetailsCollected');\n return;\n default:\n // Exhaustiveness — if Ticket #1 adds a 13th variant, this fails to compile.\n return assertNever(event);\n }\n}\n\n/**\n * Creates the framework-agnostic webhook receive handler. Returns a function\n * matching the same `HellobillRequest`/`HellobillResponse` shape used by the\n * other routes on `HellobillHandlerSet` (see `core/handler.ts`).\n *\n * Throws synchronously if `opts.webhookSecret` is falsy — a missing or\n * empty secret is caught early so that every incoming webhook does not\n * silently 401 with no visible cause.\n *\n * The Express adapter wires this in alongside `session`/`loa`/etc.\n */\nexport function createWebhookHandler(\n opts: WebhookHandlerOptions,\n): (req: HellobillRequest, res: HellobillResponse) => Promise<void> {\n // Runtime guard: `process.env.HELLOBILL_WEBHOOK_SECRET!` bang-assertion satisfies\n // the TypeScript compiler but does not catch the case where the env var is unset at\n // runtime. Without this guard, every webhook would 401 silently because\n // verifyWebhookSignature would always return false for an undefined secret.\n if (opts.webhookSecret == null || opts.webhookSecret === '') {\n throw new Error(\n 'HELLOBILL_WEBHOOK_SECRET must be set; see spec §13.2 and ' +\n 'the @hello-bill/node README \"Webhook receiver\" section.',\n );\n }\n\n const now = opts.now ?? (() => Date.now());\n const dedupeStore = opts.dedupeStore ?? new InMemoryWebhookDedupeStore(now);\n const dedupeTtlSeconds = opts.dedupeTtlSeconds ?? 86_400;\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const handlers = opts.handlers ?? {};\n // Coalesce partner-supplied Logger that may omit the optional `info` method.\n // Without this, `ctx.logger.info(...)` inside logUnhandled raises a runtime\n // TypeError when the partner Logger only provides debug/warn/error.\n const safeLogger = withInfoFallback(opts.logger ?? noopLogger);\n\n return async (req: HellobillRequest, res: HellobillResponse): Promise<void> => {\n const rawBody = req.rawBody;\n if (!rawBody) {\n safeLogger.error('hellobill.webhook.missing_raw_body', {});\n res.status(500).json({\n error: {\n type: 'api_error',\n code: 'server.internal',\n message: 'Webhook receiver requires raw body — adapter not wired correctly',\n },\n });\n return;\n }\n\n const sigHeader = headerValue(req.headers, 'x-hellobill-signature');\n if (!sigHeader) {\n res.status(401).json({ error: 'invalid_signature' });\n return;\n }\n\n const valid = verifyWebhookSignature(rawBody, sigHeader, opts.webhookSecret, {\n tolerance,\n supportedVersions,\n now,\n });\n if (!valid) {\n res.status(401).json({ error: 'invalid_signature' });\n return;\n }\n\n // Parse AFTER verification — verify operates on raw bytes by contract per spec §11.2.\n let event: WebhookEvent;\n try {\n event = JSON.parse(rawBody.toString('utf8')) as WebhookEvent;\n } catch {\n res.status(400).json({ error: 'invalid_payload' });\n return;\n }\n\n // Dedupe — record-before-dispatch so multi-replica double-delivery cannot\n // double-process. The crash-window caveat (rare loss) is documented and is\n // the at-most-once-after-2xx contract.\n const seen = await dedupeStore.has(event.id);\n if (seen) {\n safeLogger.info('hellobill.webhook.dedup_hit', { event_id: event.id, event_type: event.type });\n res.status(200).json({ received: true, deduped: true });\n return;\n }\n await dedupeStore.record(event.id, dedupeTtlSeconds);\n\n const deprecation = headerValue(req.headers, 'x-hellobill-deprecation') ?? null;\n if (deprecation) {\n safeLogger.warn('hellobill.webhook.deprecation_notice', {\n deprecation,\n event_id: event.id,\n event_type: event.type,\n });\n }\n\n // Respond 2xx FIRST. Per spec §11.1 line 2272: respond within 10s, process async.\n res.status(200).json({ received: true });\n\n const ctx: WebhookHandlerContext = {\n // safeLogger guarantees `info` is defined regardless of partner Logger shape.\n logger: safeLogger,\n headers: req.headers,\n deprecation,\n };\n\n // Fire-and-forget. Errors logged; HelloBill's retry layer is the durability mechanism.\n void dispatch(event, ctx, handlers).catch((err) => {\n safeLogger.error('hellobill.webhook.handler_error', {\n event_id: event.id,\n event_type: event.type,\n message: (err as Error).message,\n });\n });\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/webhook/verify.ts","../../src/webhook/dedupe-store.ts","../../src/webhook/handler.ts"],"names":["createHmac","timingSafeEqual"],"mappings":"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","file":"index.js","sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookEvent } from '../types/index.js';\n\nexport interface VerifyWebhookOptions {\n /** Replay window in seconds. Default 300. */\n tolerance?: number;\n /** Signature versions to accept. Default ['v1']. Use ['v1', 'v2'] during key rotation. */\n supportedVersions?: ('v1' | 'v2')[];\n /** Override `Date.now()` for tests. */\n now?: () => number;\n}\n\nexport class WebhookVerificationError extends Error {\n constructor(\n message: string,\n public code: string,\n ) {\n super(message);\n this.name = 'WebhookVerificationError';\n }\n}\n\ninterface ParsedHeader {\n t: number;\n signatures: Record<string, string>;\n}\n\nfunction parseHeader(signatureHeader: string): ParsedHeader {\n if (!signatureHeader || typeof signatureHeader !== 'string') {\n throw new WebhookVerificationError(\n 'Missing signature header',\n 'signature.missing_header',\n );\n }\n\n const parts = signatureHeader.split(',').map((p) => p.trim()).filter(Boolean);\n let t: number | undefined;\n const signatures: Record<string, string> = {};\n\n for (const part of parts) {\n const eq = part.indexOf('=');\n if (eq === -1) {\n throw new WebhookVerificationError(\n 'Malformed signature header element',\n 'signature.malformed',\n );\n }\n const key = part.slice(0, eq);\n const value = part.slice(eq + 1);\n if (key === 't') {\n t = Number(value);\n if (!Number.isFinite(t)) {\n throw new WebhookVerificationError(\n 'Malformed timestamp in signature header',\n 'signature.malformed',\n );\n }\n } else if (key.length > 0 && value.length > 0) {\n signatures[key] = value;\n }\n }\n\n if (t === undefined) {\n throw new WebhookVerificationError(\n 'Missing timestamp in signature header',\n 'signature.malformed',\n );\n }\n if (Object.keys(signatures).length === 0) {\n throw new WebhookVerificationError(\n 'No signature values in header',\n 'signature.malformed',\n );\n }\n\n return { t, signatures };\n}\n\nfunction computeHmacHex(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('hex');\n}\n\nfunction constantTimeHexEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n try {\n return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));\n } catch {\n return false;\n }\n}\n\n/**\n * Verifies a HelloBill webhook delivery.\n * Header format: `t=<unix>,v1=<hex>,v2=<hex>`.\n * HMAC computed over `${t}.${rawBody}` (raw bytes — do not re-serialise).\n */\nexport function verifyWebhook(\n rawBody: Buffer | string,\n signatureHeader: string,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): WebhookEvent {\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const now = opts.now ?? Date.now;\n\n const { t, signatures } = parseHeader(signatureHeader);\n\n const nowSeconds = Math.floor(now() / 1000);\n const skew = Math.abs(nowSeconds - t);\n if (skew > tolerance) {\n throw new WebhookVerificationError(\n `Signature timestamp outside tolerance (${skew}s > ${tolerance}s)`,\n 'signature.expired',\n );\n }\n\n const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);\n if (!hasAnyKnownVersion) {\n throw new WebhookVerificationError(\n 'No supported signature version present in header',\n 'signature.unsupported_version',\n );\n }\n\n const bodyStr =\n typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');\n const signingInput = `${t}.${bodyStr}`;\n\n for (const version of supportedVersions) {\n const provided = signatures[version];\n if (!provided) continue;\n const expected = computeHmacHex(secret, signingInput);\n if (constantTimeHexEqual(expected, provided)) {\n try {\n return JSON.parse(bodyStr) as WebhookEvent;\n } catch {\n throw new WebhookVerificationError(\n 'Verified payload is not valid JSON',\n 'signature.malformed',\n );\n }\n }\n }\n\n throw new WebhookVerificationError(\n 'Signature does not match expected value',\n 'signature.invalid',\n );\n}\n\n/**\n * Verifies a HelloBill webhook delivery without parsing the payload.\n * Returns `true` only if the timestamp is within tolerance AND at least one\n * supported scheme verifies. Returns `false` for every other case (missing/\n * malformed header, expired timestamp, no supported version, signature\n * mismatch). Never throws on verification failure — callers route on the\n * boolean. Re-throws non-verification errors (e.g. unexpected crypto failures).\n *\n * Unlike the throwing `verifyWebhook`, this function does NOT attempt to parse\n * the body as JSON — signature verification is purely over the raw bytes.\n * Callers that need the parsed event should call `verifyWebhook` directly or\n * parse separately after calling this function.\n *\n * @param rawBody Raw request body bytes (NOT re-serialised JSON).\n * @param signatureHeader Value of `X-HelloBill-Signature`.\n * @param secret Partner-issued webhook secret (env `HELLOBILL_WEBHOOK_SECRET`).\n * @param opts Optional `tolerance` (default 300s), `supportedVersions`\n * (default `['v1']`), `now` (test injection).\n */\nexport function verifyWebhookSignature(\n rawBody: Buffer | string,\n signatureHeader: string,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): boolean {\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const now = opts.now ?? Date.now;\n\n let parsed: ParsedHeader;\n try {\n parsed = parseHeader(signatureHeader);\n } catch (err) {\n if (err instanceof WebhookVerificationError) return false;\n throw err;\n }\n\n const { t, signatures } = parsed;\n\n const nowSeconds = Math.floor(now() / 1000);\n const skew = Math.abs(nowSeconds - t);\n if (skew > tolerance) return false;\n\n const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);\n if (!hasAnyKnownVersion) return false;\n\n const bodyStr =\n typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');\n const signingInput = `${t}.${bodyStr}`;\n\n for (const version of supportedVersions) {\n const provided = signatures[version];\n if (!provided) continue;\n const expected = computeHmacHex(secret, signingInput);\n if (constantTimeHexEqual(expected, provided)) {\n return true;\n }\n }\n\n return false;\n}\n","/**\n * Webhook idempotency dedupe store. Keyed on `WebhookEnvelope.id`.\n *\n * Default `InMemoryWebhookDedupeStore` is fine for single-process deployments\n * (and unit tests). Multi-replica deployments MUST inject a shared backend\n * (e.g. Redis SETEX, RDS `INSERT ... ON CONFLICT DO NOTHING`). Per spec §11.1\n * line 2270: \"Use the `id` field to deduplicate.\"\n */\nexport interface WebhookDedupeStore {\n /** Returns true if `id` has been recorded within its TTL. Never throws on miss. */\n has(id: string): Promise<boolean> | boolean;\n /** Records `id` with the given TTL in seconds. Idempotent on repeat calls. */\n record(id: string, ttlSeconds: number): Promise<void> | void;\n}\n\n/** In-memory implementation. NOT durable across process restarts. Tests + single-proc deploys. */\nexport class InMemoryWebhookDedupeStore implements WebhookDedupeStore {\n private readonly store = new Map<string, number>();\n\n // Optional test injection. Defaults to Date.now.\n constructor(private readonly now: () => number = () => Date.now()) {}\n\n async has(id: string): Promise<boolean> {\n const expiresAt = this.store.get(id);\n if (expiresAt === undefined) return false;\n // Inclusive boundary: at exactly expiresAt, treat as expired. Tested via handler test 6.5.\n if (expiresAt <= this.now()) {\n this.store.delete(id);\n return false;\n }\n return true;\n }\n\n async record(id: string, ttlSeconds: number): Promise<void> {\n this.store.set(id, this.now() + ttlSeconds * 1000);\n }\n}\n","import type {\n BankDetailsCollected,\n MoveOutNotificationFailed,\n MoveOutNotificationSent,\n SessionAccountCreated,\n SessionAppDeferred,\n SessionAppInstalled,\n SessionDetailsConfirmed,\n SessionEmbedOpened,\n SessionLoaSigned,\n SessionProductsSelected,\n SetupStatusChanged,\n SubscriptionChanged,\n WebhookEvent,\n} from '../types/index.js';\nimport type { Logger } from '../core/token-manager.js';\nimport { verifyWebhookSignature, type VerifyWebhookOptions } from './verify.js';\nimport {\n InMemoryWebhookDedupeStore,\n type WebhookDedupeStore,\n} from './dedupe-store.js';\nimport type { HellobillRequest, HellobillResponse } from '../core/handler.js';\n\n/** Per-event runtime context passed to every typed handler. */\nexport interface WebhookHandlerContext {\n /**\n * Logger guaranteed to have `info` defined — sourced from `withInfoFallback`\n * at handler construction. Partner handlers may call `ctx.logger.info(...)`\n * without a null-check regardless of what Logger literal was supplied.\n */\n logger: Logger & { info: (msg: string, meta?: Record<string, unknown>) => void };\n headers: Record<string, string | string[] | undefined>;\n deprecation: string | null;\n}\n\n/**\n * Typed handlers — one per `WebhookEvent` variant. All optional; an\n * unprovided variant logs at info (`unhandled_variant`) and the dispatcher\n * still responds 2xx so HelloBill stops retrying.\n *\n * Handlers run async AFTER the route has already responded; throwing\n * from a handler does NOT cause HelloBill to retry (a 2xx was already\n * written). Errors are logged via `opts.logger?.error`.\n */\nexport interface WebhookHandlers {\n onSessionEmbedOpened?(event: SessionEmbedOpened, ctx: WebhookHandlerContext): Promise<void>;\n onSessionDetailsConfirmed?(event: SessionDetailsConfirmed, ctx: WebhookHandlerContext): Promise<void>;\n onSessionProductsSelected?(event: SessionProductsSelected, ctx: WebhookHandlerContext): Promise<void>;\n onSessionLoaSigned?(event: SessionLoaSigned, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAccountCreated?(event: SessionAccountCreated, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAppInstalled?(event: SessionAppInstalled, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAppDeferred?(event: SessionAppDeferred, ctx: WebhookHandlerContext): Promise<void>;\n onSubscriptionChanged?(event: SubscriptionChanged, ctx: WebhookHandlerContext): Promise<void>;\n onSetupStatusChanged?(event: SetupStatusChanged, ctx: WebhookHandlerContext): Promise<void>;\n onMoveOutNotificationSent?(event: MoveOutNotificationSent, ctx: WebhookHandlerContext): Promise<void>;\n onMoveOutNotificationFailed?(event: MoveOutNotificationFailed, ctx: WebhookHandlerContext): Promise<void>;\n onBankDetailsCollected?(event: BankDetailsCollected, ctx: WebhookHandlerContext): Promise<void>;\n}\n\n/** Options for the webhook route. Lives alongside `CreateHellobillHandlerOptions`. */\nexport interface WebhookHandlerOptions {\n /**\n * Partner webhook secret. From `process.env.HELLOBILL_WEBHOOK_SECRET`.\n * MUST be a non-empty string at `createWebhookHandler` call time — the\n * factory throws synchronously on `null`/`undefined`/`''` so that an\n * unset env var is caught immediately rather than silently 401ing every\n * incoming webhook.\n */\n webhookSecret: string;\n /**\n * Optional dedupe-store. Default: in-memory (single-process). Multi-replica\n * deployments MUST supply a shared backend (Redis, RDS, etc.) — see README.\n */\n dedupeStore?: WebhookDedupeStore;\n /**\n * Idempotency window in seconds. Default 86_400 (24h). Per spec §11.1 line 2270.\n */\n dedupeTtlSeconds?: number;\n /** Signature versions accepted. Default `['v1']`. Set to `['v1','v2']` during rotation. */\n supportedVersions?: VerifyWebhookOptions['supportedVersions'];\n /** Timestamp tolerance in seconds. Default 300. */\n tolerance?: number;\n /** Optional logger. Defaults to no-op. */\n logger?: Logger;\n /** Typed handlers — provide as many as you care about; missing ones log \"unhandled\". */\n handlers?: WebhookHandlers;\n /**\n * Test injection for `Date.now()`. Applied to the signature timestamp AND,\n * only when using the default `InMemoryWebhookDedupeStore`, the dedupe TTL\n * clock. Partner-supplied `dedupeStore` instances MUST own their own clock\n * injection — `opts.now` is NOT forwarded to a partner-supplied store.\n */\n now?: () => number;\n}\n\n/**\n * Thrown by `assertNever` when an event arrives whose `type` is not in the\n * known union — typically a newer platform event that this SDK version does\n * not yet handle. The `code` discriminator lets partners identify this class\n * of error in log aggregation without an `instanceof` check.\n */\nexport class UnknownWebhookEventError extends Error {\n readonly code = 'webhook.unknown_event_type' as const;\n\n constructor(eventType: string) {\n super(`Unhandled webhook event type: ${eventType}`);\n this.name = 'UnknownWebhookEventError';\n // Restore correct prototype chain in environments where extending Error\n // loses the prototype (pre-ES2015 transpilation targets).\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Sentinel — placed on the receiver's `default:` branch to surface compile-time exhaustiveness. */\nfunction assertNever(x: never): never {\n throw new UnknownWebhookEventError((x as { type?: string }).type ?? JSON.stringify(x));\n}\n\nconst noopLogger: Logger & { info: () => void } = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n};\n\n/**\n * Coalesces a partner-supplied Logger literal that may omit the optional `info`\n * method. Without this, `ctx.logger.info(...)` inside `logUnhandled` raises a\n * runtime TypeError when the partner's Logger only provides debug/warn/error.\n */\nfunction withInfoFallback(\n logger: Logger | undefined,\n): Logger & { info: (msg: string, meta?: Record<string, unknown>) => void } {\n return {\n debug: logger?.debug?.bind(logger) ?? (() => {}),\n info: logger?.info?.bind(logger) ?? (() => {}),\n warn: logger?.warn?.bind(logger) ?? (() => {}),\n error: logger?.error?.bind(logger) ?? (() => {}),\n };\n}\n\nfunction headerValue(\n headers: Record<string, string | string[] | undefined>,\n name: string,\n): string | undefined {\n const lower = name.toLowerCase();\n for (const [k, v] of Object.entries(headers)) {\n if (k.toLowerCase() === lower) return Array.isArray(v) ? v[0] : v;\n }\n return undefined;\n}\n\n/**\n * Dispatch a verified-and-deduped event to its typed handler. Exhaustive over\n * the union. Missing handler slots fall through to `logUnhandled` so a\n * partner-only-cares-about-bank-details deployment never 500s on an\n * unrecognised event type.\n */\nasync function dispatch(\n event: WebhookEvent,\n ctx: WebhookHandlerContext,\n handlers: WebhookHandlers,\n): Promise<void> {\n const logUnhandled = (name: string): void => {\n ctx.logger.info('hellobill.webhook.unhandled_variant', {\n event_id: event.id,\n event_type: event.type,\n handler: name,\n });\n };\n\n switch (event.type) {\n case 'session.embed_opened':\n if (handlers.onSessionEmbedOpened) await handlers.onSessionEmbedOpened(event, ctx);\n else logUnhandled('onSessionEmbedOpened');\n return;\n case 'session.details_confirmed':\n if (handlers.onSessionDetailsConfirmed) await handlers.onSessionDetailsConfirmed(event, ctx);\n else logUnhandled('onSessionDetailsConfirmed');\n return;\n case 'session.products_selected':\n if (handlers.onSessionProductsSelected) await handlers.onSessionProductsSelected(event, ctx);\n else logUnhandled('onSessionProductsSelected');\n return;\n case 'session.loa_signed':\n if (handlers.onSessionLoaSigned) await handlers.onSessionLoaSigned(event, ctx);\n else logUnhandled('onSessionLoaSigned');\n return;\n case 'session.account_created':\n if (handlers.onSessionAccountCreated) await handlers.onSessionAccountCreated(event, ctx);\n else logUnhandled('onSessionAccountCreated');\n return;\n case 'session.app_installed':\n if (handlers.onSessionAppInstalled) await handlers.onSessionAppInstalled(event, ctx);\n else logUnhandled('onSessionAppInstalled');\n return;\n case 'session.app_deferred':\n if (handlers.onSessionAppDeferred) await handlers.onSessionAppDeferred(event, ctx);\n else logUnhandled('onSessionAppDeferred');\n return;\n case 'subscription.changed':\n if (handlers.onSubscriptionChanged) await handlers.onSubscriptionChanged(event, ctx);\n else logUnhandled('onSubscriptionChanged');\n return;\n case 'setup_status.changed':\n if (handlers.onSetupStatusChanged) await handlers.onSetupStatusChanged(event, ctx);\n else logUnhandled('onSetupStatusChanged');\n return;\n case 'move_out_notification.sent':\n if (handlers.onMoveOutNotificationSent) await handlers.onMoveOutNotificationSent(event, ctx);\n else logUnhandled('onMoveOutNotificationSent');\n return;\n case 'move_out_notification.failed':\n if (handlers.onMoveOutNotificationFailed) await handlers.onMoveOutNotificationFailed(event, ctx);\n else logUnhandled('onMoveOutNotificationFailed');\n return;\n case 'bank_details.collected':\n if (handlers.onBankDetailsCollected) await handlers.onBankDetailsCollected(event, ctx);\n else logUnhandled('onBankDetailsCollected');\n return;\n default:\n // Exhaustiveness — if Ticket #1 adds a 13th variant, this fails to compile.\n return assertNever(event);\n }\n}\n\n/**\n * Creates the framework-agnostic webhook receive handler. Returns a function\n * matching the same `HellobillRequest`/`HellobillResponse` shape used by the\n * other routes on `HellobillHandlerSet` (see `core/handler.ts`).\n *\n * Throws synchronously if `opts.webhookSecret` is falsy — a missing or\n * empty secret is caught early so that every incoming webhook does not\n * silently 401 with no visible cause.\n *\n * The Express adapter wires this in alongside `session`/`loa`/etc.\n */\nexport function createWebhookHandler(\n opts: WebhookHandlerOptions,\n): (req: HellobillRequest, res: HellobillResponse) => Promise<void> {\n // Runtime guard: `process.env.HELLOBILL_WEBHOOK_SECRET!` bang-assertion satisfies\n // the TypeScript compiler but does not catch the case where the env var is unset at\n // runtime. Without this guard, every webhook would 401 silently because\n // verifyWebhookSignature would always return false for an undefined secret.\n if (opts.webhookSecret == null || opts.webhookSecret === '') {\n throw new Error(\n 'HELLOBILL_WEBHOOK_SECRET must be set; see spec §13.2 and ' +\n 'the @hello-bill/node README \"Webhook receiver\" section.',\n );\n }\n\n const now = opts.now ?? (() => Date.now());\n const dedupeStore = opts.dedupeStore ?? new InMemoryWebhookDedupeStore(now);\n const dedupeTtlSeconds = opts.dedupeTtlSeconds ?? 86_400;\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const handlers = opts.handlers ?? {};\n // Coalesce partner-supplied Logger that may omit the optional `info` method.\n // Without this, `ctx.logger.info(...)` inside logUnhandled raises a runtime\n // TypeError when the partner Logger only provides debug/warn/error.\n const safeLogger = withInfoFallback(opts.logger ?? noopLogger);\n\n return async (req: HellobillRequest, res: HellobillResponse): Promise<void> => {\n const rawBody = req.rawBody;\n if (!rawBody) {\n safeLogger.error('hellobill.webhook.missing_raw_body', {});\n res.status(500).json({\n error: {\n type: 'api_error',\n code: 'server.internal',\n message: 'Webhook receiver requires raw body — adapter not wired correctly',\n },\n });\n return;\n }\n\n const sigHeader = headerValue(req.headers, 'x-hellobill-signature');\n if (!sigHeader) {\n res.status(401).json({ error: 'invalid_signature' });\n return;\n }\n\n const valid = verifyWebhookSignature(rawBody, sigHeader, opts.webhookSecret, {\n tolerance,\n supportedVersions,\n now,\n });\n if (!valid) {\n res.status(401).json({ error: 'invalid_signature' });\n return;\n }\n\n // Parse AFTER verification — verify operates on raw bytes by contract per spec §11.2.\n let event: WebhookEvent;\n try {\n event = JSON.parse(rawBody.toString('utf8')) as WebhookEvent;\n } catch {\n res.status(400).json({ error: 'invalid_payload' });\n return;\n }\n\n // Dedupe — record-before-dispatch so multi-replica double-delivery cannot\n // double-process. The crash-window caveat (rare loss) is documented and is\n // the at-most-once-after-2xx contract.\n const seen = await dedupeStore.has(event.id);\n if (seen) {\n safeLogger.info('hellobill.webhook.dedup_hit', { event_id: event.id, event_type: event.type });\n res.status(200).json({ received: true, deduped: true });\n return;\n }\n await dedupeStore.record(event.id, dedupeTtlSeconds);\n\n const deprecation = headerValue(req.headers, 'x-hellobill-deprecation') ?? null;\n if (deprecation) {\n safeLogger.warn('hellobill.webhook.deprecation_notice', {\n deprecation,\n event_id: event.id,\n event_type: event.type,\n });\n }\n\n // Respond 2xx FIRST. Per spec §11.1 line 2272: respond within 10s, process async.\n res.status(200).json({ received: true });\n\n const ctx: WebhookHandlerContext = {\n // safeLogger guarantees `info` is defined regardless of partner Logger shape.\n logger: safeLogger,\n headers: req.headers,\n deprecation,\n };\n\n // Fire-and-forget. Errors logged; HelloBill's retry layer is the durability mechanism.\n void dispatch(event, ctx, handlers).catch((err) => {\n safeLogger.error('hellobill.webhook.handler_error', {\n event_id: event.id,\n event_type: event.type,\n message: (err as Error).message,\n // Include the typed error code when present so partners can identify\n // known error classes (e.g. unknown event type) in log aggregation.\n ...(err != null && typeof err === 'object' && 'code' in err\n ? { code: (err as { code: unknown }).code }\n : {}),\n });\n });\n };\n}\n"]}
@@ -159,8 +159,16 @@ var InMemoryWebhookDedupeStore = class {
159
159
  };
160
160
 
161
161
  // src/webhook/handler.ts
162
+ var UnknownWebhookEventError = class extends Error {
163
+ code = "webhook.unknown_event_type";
164
+ constructor(eventType) {
165
+ super(`Unhandled webhook event type: ${eventType}`);
166
+ this.name = "UnknownWebhookEventError";
167
+ Object.setPrototypeOf(this, new.target.prototype);
168
+ }
169
+ };
162
170
  function assertNever(x) {
163
- throw new Error(`Unhandled webhook variant: ${JSON.stringify(x)}`);
171
+ throw new UnknownWebhookEventError(x.type ?? JSON.stringify(x));
164
172
  }
165
173
  var noopLogger = {
166
174
  debug: () => {
@@ -325,12 +333,15 @@ function createWebhookHandler(opts) {
325
333
  safeLogger.error("hellobill.webhook.handler_error", {
326
334
  event_id: event.id,
327
335
  event_type: event.type,
328
- message: err.message
336
+ message: err.message,
337
+ // Include the typed error code when present so partners can identify
338
+ // known error classes (e.g. unknown event type) in log aggregation.
339
+ ...err != null && typeof err === "object" && "code" in err ? { code: err.code } : {}
329
340
  });
330
341
  });
331
342
  };
332
343
  }
333
344
 
334
- export { InMemoryWebhookDedupeStore, WebhookVerificationError, createWebhookHandler, verifyWebhook, verifyWebhookSignature };
345
+ export { InMemoryWebhookDedupeStore, UnknownWebhookEventError, WebhookVerificationError, createWebhookHandler, verifyWebhook, verifyWebhookSignature };
335
346
  //# sourceMappingURL=index.mjs.map
336
347
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/webhook/verify.ts","../../src/webhook/dedupe-store.ts","../../src/webhook/handler.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookEvent } from '../types/index.js';\n\nexport interface VerifyWebhookOptions {\n /** Replay window in seconds. Default 300. */\n tolerance?: number;\n /** Signature versions to accept. Default ['v1']. Use ['v1', 'v2'] during key rotation. */\n supportedVersions?: ('v1' | 'v2')[];\n /** Override `Date.now()` for tests. */\n now?: () => number;\n}\n\nexport class WebhookVerificationError extends Error {\n constructor(\n message: string,\n public code: string,\n ) {\n super(message);\n this.name = 'WebhookVerificationError';\n }\n}\n\ninterface ParsedHeader {\n t: number;\n signatures: Record<string, string>;\n}\n\nfunction parseHeader(signatureHeader: string): ParsedHeader {\n if (!signatureHeader || typeof signatureHeader !== 'string') {\n throw new WebhookVerificationError(\n 'Missing signature header',\n 'signature.missing_header',\n );\n }\n\n const parts = signatureHeader.split(',').map((p) => p.trim()).filter(Boolean);\n let t: number | undefined;\n const signatures: Record<string, string> = {};\n\n for (const part of parts) {\n const eq = part.indexOf('=');\n if (eq === -1) {\n throw new WebhookVerificationError(\n 'Malformed signature header element',\n 'signature.malformed',\n );\n }\n const key = part.slice(0, eq);\n const value = part.slice(eq + 1);\n if (key === 't') {\n t = Number(value);\n if (!Number.isFinite(t)) {\n throw new WebhookVerificationError(\n 'Malformed timestamp in signature header',\n 'signature.malformed',\n );\n }\n } else if (key.length > 0 && value.length > 0) {\n signatures[key] = value;\n }\n }\n\n if (t === undefined) {\n throw new WebhookVerificationError(\n 'Missing timestamp in signature header',\n 'signature.malformed',\n );\n }\n if (Object.keys(signatures).length === 0) {\n throw new WebhookVerificationError(\n 'No signature values in header',\n 'signature.malformed',\n );\n }\n\n return { t, signatures };\n}\n\nfunction computeHmacHex(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('hex');\n}\n\nfunction constantTimeHexEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n try {\n return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));\n } catch {\n return false;\n }\n}\n\n/**\n * Verifies a HelloBill webhook delivery.\n * Header format: `t=<unix>,v1=<hex>,v2=<hex>`.\n * HMAC computed over `${t}.${rawBody}` (raw bytes — do not re-serialise).\n */\nexport function verifyWebhook(\n rawBody: Buffer | string,\n signatureHeader: string,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): WebhookEvent {\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const now = opts.now ?? Date.now;\n\n const { t, signatures } = parseHeader(signatureHeader);\n\n const nowSeconds = Math.floor(now() / 1000);\n const skew = Math.abs(nowSeconds - t);\n if (skew > tolerance) {\n throw new WebhookVerificationError(\n `Signature timestamp outside tolerance (${skew}s > ${tolerance}s)`,\n 'signature.expired',\n );\n }\n\n const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);\n if (!hasAnyKnownVersion) {\n throw new WebhookVerificationError(\n 'No supported signature version present in header',\n 'signature.unsupported_version',\n );\n }\n\n const bodyStr =\n typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');\n const signingInput = `${t}.${bodyStr}`;\n\n for (const version of supportedVersions) {\n const provided = signatures[version];\n if (!provided) continue;\n const expected = computeHmacHex(secret, signingInput);\n if (constantTimeHexEqual(expected, provided)) {\n try {\n return JSON.parse(bodyStr) as WebhookEvent;\n } catch {\n throw new WebhookVerificationError(\n 'Verified payload is not valid JSON',\n 'signature.malformed',\n );\n }\n }\n }\n\n throw new WebhookVerificationError(\n 'Signature does not match expected value',\n 'signature.invalid',\n );\n}\n\n/**\n * Verifies a HelloBill webhook delivery without parsing the payload.\n * Returns `true` only if the timestamp is within tolerance AND at least one\n * supported scheme verifies. Returns `false` for every other case (missing/\n * malformed header, expired timestamp, no supported version, signature\n * mismatch). Never throws on verification failure — callers route on the\n * boolean. Re-throws non-verification errors (e.g. unexpected crypto failures).\n *\n * Unlike the throwing `verifyWebhook`, this function does NOT attempt to parse\n * the body as JSON — signature verification is purely over the raw bytes.\n * Callers that need the parsed event should call `verifyWebhook` directly or\n * parse separately after calling this function.\n *\n * @param rawBody Raw request body bytes (NOT re-serialised JSON).\n * @param signatureHeader Value of `X-HelloBill-Signature`.\n * @param secret Partner-issued webhook secret (env `HELLOBILL_WEBHOOK_SECRET`).\n * @param opts Optional `tolerance` (default 300s), `supportedVersions`\n * (default `['v1']`), `now` (test injection).\n */\nexport function verifyWebhookSignature(\n rawBody: Buffer | string,\n signatureHeader: string,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): boolean {\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const now = opts.now ?? Date.now;\n\n let parsed: ParsedHeader;\n try {\n parsed = parseHeader(signatureHeader);\n } catch (err) {\n if (err instanceof WebhookVerificationError) return false;\n throw err;\n }\n\n const { t, signatures } = parsed;\n\n const nowSeconds = Math.floor(now() / 1000);\n const skew = Math.abs(nowSeconds - t);\n if (skew > tolerance) return false;\n\n const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);\n if (!hasAnyKnownVersion) return false;\n\n const bodyStr =\n typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');\n const signingInput = `${t}.${bodyStr}`;\n\n for (const version of supportedVersions) {\n const provided = signatures[version];\n if (!provided) continue;\n const expected = computeHmacHex(secret, signingInput);\n if (constantTimeHexEqual(expected, provided)) {\n return true;\n }\n }\n\n return false;\n}\n","/**\n * Webhook idempotency dedupe store. Keyed on `WebhookEnvelope.id`.\n *\n * Default `InMemoryWebhookDedupeStore` is fine for single-process deployments\n * (and unit tests). Multi-replica deployments MUST inject a shared backend\n * (e.g. Redis SETEX, RDS `INSERT ... ON CONFLICT DO NOTHING`). Per spec §11.1\n * line 2270: \"Use the `id` field to deduplicate.\"\n */\nexport interface WebhookDedupeStore {\n /** Returns true if `id` has been recorded within its TTL. Never throws on miss. */\n has(id: string): Promise<boolean> | boolean;\n /** Records `id` with the given TTL in seconds. Idempotent on repeat calls. */\n record(id: string, ttlSeconds: number): Promise<void> | void;\n}\n\n/** In-memory implementation. NOT durable across process restarts. Tests + single-proc deploys. */\nexport class InMemoryWebhookDedupeStore implements WebhookDedupeStore {\n private readonly store = new Map<string, number>();\n\n // Optional test injection. Defaults to Date.now.\n constructor(private readonly now: () => number = () => Date.now()) {}\n\n async has(id: string): Promise<boolean> {\n const expiresAt = this.store.get(id);\n if (expiresAt === undefined) return false;\n // Inclusive boundary: at exactly expiresAt, treat as expired. Tested via handler test 6.5.\n if (expiresAt <= this.now()) {\n this.store.delete(id);\n return false;\n }\n return true;\n }\n\n async record(id: string, ttlSeconds: number): Promise<void> {\n this.store.set(id, this.now() + ttlSeconds * 1000);\n }\n}\n","import type {\n BankDetailsCollected,\n MoveOutNotificationFailed,\n MoveOutNotificationSent,\n SessionAccountCreated,\n SessionAppDeferred,\n SessionAppInstalled,\n SessionDetailsConfirmed,\n SessionEmbedOpened,\n SessionLoaSigned,\n SessionProductsSelected,\n SetupStatusChanged,\n SubscriptionChanged,\n WebhookEvent,\n} from '../types/index.js';\nimport type { Logger } from '../core/token-manager.js';\nimport { verifyWebhookSignature, type VerifyWebhookOptions } from './verify.js';\nimport {\n InMemoryWebhookDedupeStore,\n type WebhookDedupeStore,\n} from './dedupe-store.js';\nimport type { HellobillRequest, HellobillResponse } from '../core/handler.js';\n\n/** Per-event runtime context passed to every typed handler. */\nexport interface WebhookHandlerContext {\n /**\n * Logger guaranteed to have `info` defined — sourced from `withInfoFallback`\n * at handler construction. Partner handlers may call `ctx.logger.info(...)`\n * without a null-check regardless of what Logger literal was supplied.\n */\n logger: Logger & { info: (msg: string, meta?: Record<string, unknown>) => void };\n headers: Record<string, string | string[] | undefined>;\n deprecation: string | null;\n}\n\n/**\n * Typed handlers — one per `WebhookEvent` variant. All optional; an\n * unprovided variant logs at info (`unhandled_variant`) and the dispatcher\n * still responds 2xx so HelloBill stops retrying.\n *\n * Handlers run async AFTER the route has already responded; throwing\n * from a handler does NOT cause HelloBill to retry (a 2xx was already\n * written). Errors are logged via `opts.logger?.error`.\n */\nexport interface WebhookHandlers {\n onSessionEmbedOpened?(event: SessionEmbedOpened, ctx: WebhookHandlerContext): Promise<void>;\n onSessionDetailsConfirmed?(event: SessionDetailsConfirmed, ctx: WebhookHandlerContext): Promise<void>;\n onSessionProductsSelected?(event: SessionProductsSelected, ctx: WebhookHandlerContext): Promise<void>;\n onSessionLoaSigned?(event: SessionLoaSigned, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAccountCreated?(event: SessionAccountCreated, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAppInstalled?(event: SessionAppInstalled, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAppDeferred?(event: SessionAppDeferred, ctx: WebhookHandlerContext): Promise<void>;\n onSubscriptionChanged?(event: SubscriptionChanged, ctx: WebhookHandlerContext): Promise<void>;\n onSetupStatusChanged?(event: SetupStatusChanged, ctx: WebhookHandlerContext): Promise<void>;\n onMoveOutNotificationSent?(event: MoveOutNotificationSent, ctx: WebhookHandlerContext): Promise<void>;\n onMoveOutNotificationFailed?(event: MoveOutNotificationFailed, ctx: WebhookHandlerContext): Promise<void>;\n onBankDetailsCollected?(event: BankDetailsCollected, ctx: WebhookHandlerContext): Promise<void>;\n}\n\n/** Options for the webhook route. Lives alongside `CreateHellobillHandlerOptions`. */\nexport interface WebhookHandlerOptions {\n /**\n * Partner webhook secret. From `process.env.HELLOBILL_WEBHOOK_SECRET`.\n * MUST be a non-empty string at `createWebhookHandler` call time — the\n * factory throws synchronously on `null`/`undefined`/`''` so that an\n * unset env var is caught immediately rather than silently 401ing every\n * incoming webhook.\n */\n webhookSecret: string;\n /**\n * Optional dedupe-store. Default: in-memory (single-process). Multi-replica\n * deployments MUST supply a shared backend (Redis, RDS, etc.) — see README.\n */\n dedupeStore?: WebhookDedupeStore;\n /**\n * Idempotency window in seconds. Default 86_400 (24h). Per spec §11.1 line 2270.\n */\n dedupeTtlSeconds?: number;\n /** Signature versions accepted. Default `['v1']`. Set to `['v1','v2']` during rotation. */\n supportedVersions?: VerifyWebhookOptions['supportedVersions'];\n /** Timestamp tolerance in seconds. Default 300. */\n tolerance?: number;\n /** Optional logger. Defaults to no-op. */\n logger?: Logger;\n /** Typed handlers — provide as many as you care about; missing ones log \"unhandled\". */\n handlers?: WebhookHandlers;\n /**\n * Test injection for `Date.now()`. Applied to the signature timestamp AND,\n * only when using the default `InMemoryWebhookDedupeStore`, the dedupe TTL\n * clock. Partner-supplied `dedupeStore` instances MUST own their own clock\n * injection — `opts.now` is NOT forwarded to a partner-supplied store.\n */\n now?: () => number;\n}\n\n/** Sentinel — placed on the receiver's `default:` branch to surface compile-time exhaustiveness. */\nfunction assertNever(x: never): never {\n throw new Error(`Unhandled webhook variant: ${JSON.stringify(x)}`);\n}\n\nconst noopLogger: Logger & { info: () => void } = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n};\n\n/**\n * Coalesces a partner-supplied Logger literal that may omit the optional `info`\n * method. Without this, `ctx.logger.info(...)` inside `logUnhandled` raises a\n * runtime TypeError when the partner's Logger only provides debug/warn/error.\n */\nfunction withInfoFallback(\n logger: Logger | undefined,\n): Logger & { info: (msg: string, meta?: Record<string, unknown>) => void } {\n return {\n debug: logger?.debug?.bind(logger) ?? (() => {}),\n info: logger?.info?.bind(logger) ?? (() => {}),\n warn: logger?.warn?.bind(logger) ?? (() => {}),\n error: logger?.error?.bind(logger) ?? (() => {}),\n };\n}\n\nfunction headerValue(\n headers: Record<string, string | string[] | undefined>,\n name: string,\n): string | undefined {\n const lower = name.toLowerCase();\n for (const [k, v] of Object.entries(headers)) {\n if (k.toLowerCase() === lower) return Array.isArray(v) ? v[0] : v;\n }\n return undefined;\n}\n\n/**\n * Dispatch a verified-and-deduped event to its typed handler. Exhaustive over\n * the union. Missing handler slots fall through to `logUnhandled` so a\n * partner-only-cares-about-bank-details deployment never 500s on an\n * unrecognised event type.\n */\nasync function dispatch(\n event: WebhookEvent,\n ctx: WebhookHandlerContext,\n handlers: WebhookHandlers,\n): Promise<void> {\n const logUnhandled = (name: string): void => {\n ctx.logger.info('hellobill.webhook.unhandled_variant', {\n event_id: event.id,\n event_type: event.type,\n handler: name,\n });\n };\n\n switch (event.type) {\n case 'session.embed_opened':\n if (handlers.onSessionEmbedOpened) await handlers.onSessionEmbedOpened(event, ctx);\n else logUnhandled('onSessionEmbedOpened');\n return;\n case 'session.details_confirmed':\n if (handlers.onSessionDetailsConfirmed) await handlers.onSessionDetailsConfirmed(event, ctx);\n else logUnhandled('onSessionDetailsConfirmed');\n return;\n case 'session.products_selected':\n if (handlers.onSessionProductsSelected) await handlers.onSessionProductsSelected(event, ctx);\n else logUnhandled('onSessionProductsSelected');\n return;\n case 'session.loa_signed':\n if (handlers.onSessionLoaSigned) await handlers.onSessionLoaSigned(event, ctx);\n else logUnhandled('onSessionLoaSigned');\n return;\n case 'session.account_created':\n if (handlers.onSessionAccountCreated) await handlers.onSessionAccountCreated(event, ctx);\n else logUnhandled('onSessionAccountCreated');\n return;\n case 'session.app_installed':\n if (handlers.onSessionAppInstalled) await handlers.onSessionAppInstalled(event, ctx);\n else logUnhandled('onSessionAppInstalled');\n return;\n case 'session.app_deferred':\n if (handlers.onSessionAppDeferred) await handlers.onSessionAppDeferred(event, ctx);\n else logUnhandled('onSessionAppDeferred');\n return;\n case 'subscription.changed':\n if (handlers.onSubscriptionChanged) await handlers.onSubscriptionChanged(event, ctx);\n else logUnhandled('onSubscriptionChanged');\n return;\n case 'setup_status.changed':\n if (handlers.onSetupStatusChanged) await handlers.onSetupStatusChanged(event, ctx);\n else logUnhandled('onSetupStatusChanged');\n return;\n case 'move_out_notification.sent':\n if (handlers.onMoveOutNotificationSent) await handlers.onMoveOutNotificationSent(event, ctx);\n else logUnhandled('onMoveOutNotificationSent');\n return;\n case 'move_out_notification.failed':\n if (handlers.onMoveOutNotificationFailed) await handlers.onMoveOutNotificationFailed(event, ctx);\n else logUnhandled('onMoveOutNotificationFailed');\n return;\n case 'bank_details.collected':\n if (handlers.onBankDetailsCollected) await handlers.onBankDetailsCollected(event, ctx);\n else logUnhandled('onBankDetailsCollected');\n return;\n default:\n // Exhaustiveness — if Ticket #1 adds a 13th variant, this fails to compile.\n return assertNever(event);\n }\n}\n\n/**\n * Creates the framework-agnostic webhook receive handler. Returns a function\n * matching the same `HellobillRequest`/`HellobillResponse` shape used by the\n * other routes on `HellobillHandlerSet` (see `core/handler.ts`).\n *\n * Throws synchronously if `opts.webhookSecret` is falsy — a missing or\n * empty secret is caught early so that every incoming webhook does not\n * silently 401 with no visible cause.\n *\n * The Express adapter wires this in alongside `session`/`loa`/etc.\n */\nexport function createWebhookHandler(\n opts: WebhookHandlerOptions,\n): (req: HellobillRequest, res: HellobillResponse) => Promise<void> {\n // Runtime guard: `process.env.HELLOBILL_WEBHOOK_SECRET!` bang-assertion satisfies\n // the TypeScript compiler but does not catch the case where the env var is unset at\n // runtime. Without this guard, every webhook would 401 silently because\n // verifyWebhookSignature would always return false for an undefined secret.\n if (opts.webhookSecret == null || opts.webhookSecret === '') {\n throw new Error(\n 'HELLOBILL_WEBHOOK_SECRET must be set; see spec §13.2 and ' +\n 'the @hello-bill/node README \"Webhook receiver\" section.',\n );\n }\n\n const now = opts.now ?? (() => Date.now());\n const dedupeStore = opts.dedupeStore ?? new InMemoryWebhookDedupeStore(now);\n const dedupeTtlSeconds = opts.dedupeTtlSeconds ?? 86_400;\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const handlers = opts.handlers ?? {};\n // Coalesce partner-supplied Logger that may omit the optional `info` method.\n // Without this, `ctx.logger.info(...)` inside logUnhandled raises a runtime\n // TypeError when the partner Logger only provides debug/warn/error.\n const safeLogger = withInfoFallback(opts.logger ?? noopLogger);\n\n return async (req: HellobillRequest, res: HellobillResponse): Promise<void> => {\n const rawBody = req.rawBody;\n if (!rawBody) {\n safeLogger.error('hellobill.webhook.missing_raw_body', {});\n res.status(500).json({\n error: {\n type: 'api_error',\n code: 'server.internal',\n message: 'Webhook receiver requires raw body — adapter not wired correctly',\n },\n });\n return;\n }\n\n const sigHeader = headerValue(req.headers, 'x-hellobill-signature');\n if (!sigHeader) {\n res.status(401).json({ error: 'invalid_signature' });\n return;\n }\n\n const valid = verifyWebhookSignature(rawBody, sigHeader, opts.webhookSecret, {\n tolerance,\n supportedVersions,\n now,\n });\n if (!valid) {\n res.status(401).json({ error: 'invalid_signature' });\n return;\n }\n\n // Parse AFTER verification — verify operates on raw bytes by contract per spec §11.2.\n let event: WebhookEvent;\n try {\n event = JSON.parse(rawBody.toString('utf8')) as WebhookEvent;\n } catch {\n res.status(400).json({ error: 'invalid_payload' });\n return;\n }\n\n // Dedupe — record-before-dispatch so multi-replica double-delivery cannot\n // double-process. The crash-window caveat (rare loss) is documented and is\n // the at-most-once-after-2xx contract.\n const seen = await dedupeStore.has(event.id);\n if (seen) {\n safeLogger.info('hellobill.webhook.dedup_hit', { event_id: event.id, event_type: event.type });\n res.status(200).json({ received: true, deduped: true });\n return;\n }\n await dedupeStore.record(event.id, dedupeTtlSeconds);\n\n const deprecation = headerValue(req.headers, 'x-hellobill-deprecation') ?? null;\n if (deprecation) {\n safeLogger.warn('hellobill.webhook.deprecation_notice', {\n deprecation,\n event_id: event.id,\n event_type: event.type,\n });\n }\n\n // Respond 2xx FIRST. Per spec §11.1 line 2272: respond within 10s, process async.\n res.status(200).json({ received: true });\n\n const ctx: WebhookHandlerContext = {\n // safeLogger guarantees `info` is defined regardless of partner Logger shape.\n logger: safeLogger,\n headers: req.headers,\n deprecation,\n };\n\n // Fire-and-forget. Errors logged; HelloBill's retry layer is the durability mechanism.\n void dispatch(event, ctx, handlers).catch((err) => {\n safeLogger.error('hellobill.webhook.handler_error', {\n event_id: event.id,\n event_type: event.type,\n message: (err as Error).message,\n });\n });\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/webhook/verify.ts","../../src/webhook/dedupe-store.ts","../../src/webhook/handler.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookEvent } from '../types/index.js';\n\nexport interface VerifyWebhookOptions {\n /** Replay window in seconds. Default 300. */\n tolerance?: number;\n /** Signature versions to accept. Default ['v1']. Use ['v1', 'v2'] during key rotation. */\n supportedVersions?: ('v1' | 'v2')[];\n /** Override `Date.now()` for tests. */\n now?: () => number;\n}\n\nexport class WebhookVerificationError extends Error {\n constructor(\n message: string,\n public code: string,\n ) {\n super(message);\n this.name = 'WebhookVerificationError';\n }\n}\n\ninterface ParsedHeader {\n t: number;\n signatures: Record<string, string>;\n}\n\nfunction parseHeader(signatureHeader: string): ParsedHeader {\n if (!signatureHeader || typeof signatureHeader !== 'string') {\n throw new WebhookVerificationError(\n 'Missing signature header',\n 'signature.missing_header',\n );\n }\n\n const parts = signatureHeader.split(',').map((p) => p.trim()).filter(Boolean);\n let t: number | undefined;\n const signatures: Record<string, string> = {};\n\n for (const part of parts) {\n const eq = part.indexOf('=');\n if (eq === -1) {\n throw new WebhookVerificationError(\n 'Malformed signature header element',\n 'signature.malformed',\n );\n }\n const key = part.slice(0, eq);\n const value = part.slice(eq + 1);\n if (key === 't') {\n t = Number(value);\n if (!Number.isFinite(t)) {\n throw new WebhookVerificationError(\n 'Malformed timestamp in signature header',\n 'signature.malformed',\n );\n }\n } else if (key.length > 0 && value.length > 0) {\n signatures[key] = value;\n }\n }\n\n if (t === undefined) {\n throw new WebhookVerificationError(\n 'Missing timestamp in signature header',\n 'signature.malformed',\n );\n }\n if (Object.keys(signatures).length === 0) {\n throw new WebhookVerificationError(\n 'No signature values in header',\n 'signature.malformed',\n );\n }\n\n return { t, signatures };\n}\n\nfunction computeHmacHex(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('hex');\n}\n\nfunction constantTimeHexEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n try {\n return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));\n } catch {\n return false;\n }\n}\n\n/**\n * Verifies a HelloBill webhook delivery.\n * Header format: `t=<unix>,v1=<hex>,v2=<hex>`.\n * HMAC computed over `${t}.${rawBody}` (raw bytes — do not re-serialise).\n */\nexport function verifyWebhook(\n rawBody: Buffer | string,\n signatureHeader: string,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): WebhookEvent {\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const now = opts.now ?? Date.now;\n\n const { t, signatures } = parseHeader(signatureHeader);\n\n const nowSeconds = Math.floor(now() / 1000);\n const skew = Math.abs(nowSeconds - t);\n if (skew > tolerance) {\n throw new WebhookVerificationError(\n `Signature timestamp outside tolerance (${skew}s > ${tolerance}s)`,\n 'signature.expired',\n );\n }\n\n const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);\n if (!hasAnyKnownVersion) {\n throw new WebhookVerificationError(\n 'No supported signature version present in header',\n 'signature.unsupported_version',\n );\n }\n\n const bodyStr =\n typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');\n const signingInput = `${t}.${bodyStr}`;\n\n for (const version of supportedVersions) {\n const provided = signatures[version];\n if (!provided) continue;\n const expected = computeHmacHex(secret, signingInput);\n if (constantTimeHexEqual(expected, provided)) {\n try {\n return JSON.parse(bodyStr) as WebhookEvent;\n } catch {\n throw new WebhookVerificationError(\n 'Verified payload is not valid JSON',\n 'signature.malformed',\n );\n }\n }\n }\n\n throw new WebhookVerificationError(\n 'Signature does not match expected value',\n 'signature.invalid',\n );\n}\n\n/**\n * Verifies a HelloBill webhook delivery without parsing the payload.\n * Returns `true` only if the timestamp is within tolerance AND at least one\n * supported scheme verifies. Returns `false` for every other case (missing/\n * malformed header, expired timestamp, no supported version, signature\n * mismatch). Never throws on verification failure — callers route on the\n * boolean. Re-throws non-verification errors (e.g. unexpected crypto failures).\n *\n * Unlike the throwing `verifyWebhook`, this function does NOT attempt to parse\n * the body as JSON — signature verification is purely over the raw bytes.\n * Callers that need the parsed event should call `verifyWebhook` directly or\n * parse separately after calling this function.\n *\n * @param rawBody Raw request body bytes (NOT re-serialised JSON).\n * @param signatureHeader Value of `X-HelloBill-Signature`.\n * @param secret Partner-issued webhook secret (env `HELLOBILL_WEBHOOK_SECRET`).\n * @param opts Optional `tolerance` (default 300s), `supportedVersions`\n * (default `['v1']`), `now` (test injection).\n */\nexport function verifyWebhookSignature(\n rawBody: Buffer | string,\n signatureHeader: string,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): boolean {\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const now = opts.now ?? Date.now;\n\n let parsed: ParsedHeader;\n try {\n parsed = parseHeader(signatureHeader);\n } catch (err) {\n if (err instanceof WebhookVerificationError) return false;\n throw err;\n }\n\n const { t, signatures } = parsed;\n\n const nowSeconds = Math.floor(now() / 1000);\n const skew = Math.abs(nowSeconds - t);\n if (skew > tolerance) return false;\n\n const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);\n if (!hasAnyKnownVersion) return false;\n\n const bodyStr =\n typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');\n const signingInput = `${t}.${bodyStr}`;\n\n for (const version of supportedVersions) {\n const provided = signatures[version];\n if (!provided) continue;\n const expected = computeHmacHex(secret, signingInput);\n if (constantTimeHexEqual(expected, provided)) {\n return true;\n }\n }\n\n return false;\n}\n","/**\n * Webhook idempotency dedupe store. Keyed on `WebhookEnvelope.id`.\n *\n * Default `InMemoryWebhookDedupeStore` is fine for single-process deployments\n * (and unit tests). Multi-replica deployments MUST inject a shared backend\n * (e.g. Redis SETEX, RDS `INSERT ... ON CONFLICT DO NOTHING`). Per spec §11.1\n * line 2270: \"Use the `id` field to deduplicate.\"\n */\nexport interface WebhookDedupeStore {\n /** Returns true if `id` has been recorded within its TTL. Never throws on miss. */\n has(id: string): Promise<boolean> | boolean;\n /** Records `id` with the given TTL in seconds. Idempotent on repeat calls. */\n record(id: string, ttlSeconds: number): Promise<void> | void;\n}\n\n/** In-memory implementation. NOT durable across process restarts. Tests + single-proc deploys. */\nexport class InMemoryWebhookDedupeStore implements WebhookDedupeStore {\n private readonly store = new Map<string, number>();\n\n // Optional test injection. Defaults to Date.now.\n constructor(private readonly now: () => number = () => Date.now()) {}\n\n async has(id: string): Promise<boolean> {\n const expiresAt = this.store.get(id);\n if (expiresAt === undefined) return false;\n // Inclusive boundary: at exactly expiresAt, treat as expired. Tested via handler test 6.5.\n if (expiresAt <= this.now()) {\n this.store.delete(id);\n return false;\n }\n return true;\n }\n\n async record(id: string, ttlSeconds: number): Promise<void> {\n this.store.set(id, this.now() + ttlSeconds * 1000);\n }\n}\n","import type {\n BankDetailsCollected,\n MoveOutNotificationFailed,\n MoveOutNotificationSent,\n SessionAccountCreated,\n SessionAppDeferred,\n SessionAppInstalled,\n SessionDetailsConfirmed,\n SessionEmbedOpened,\n SessionLoaSigned,\n SessionProductsSelected,\n SetupStatusChanged,\n SubscriptionChanged,\n WebhookEvent,\n} from '../types/index.js';\nimport type { Logger } from '../core/token-manager.js';\nimport { verifyWebhookSignature, type VerifyWebhookOptions } from './verify.js';\nimport {\n InMemoryWebhookDedupeStore,\n type WebhookDedupeStore,\n} from './dedupe-store.js';\nimport type { HellobillRequest, HellobillResponse } from '../core/handler.js';\n\n/** Per-event runtime context passed to every typed handler. */\nexport interface WebhookHandlerContext {\n /**\n * Logger guaranteed to have `info` defined — sourced from `withInfoFallback`\n * at handler construction. Partner handlers may call `ctx.logger.info(...)`\n * without a null-check regardless of what Logger literal was supplied.\n */\n logger: Logger & { info: (msg: string, meta?: Record<string, unknown>) => void };\n headers: Record<string, string | string[] | undefined>;\n deprecation: string | null;\n}\n\n/**\n * Typed handlers — one per `WebhookEvent` variant. All optional; an\n * unprovided variant logs at info (`unhandled_variant`) and the dispatcher\n * still responds 2xx so HelloBill stops retrying.\n *\n * Handlers run async AFTER the route has already responded; throwing\n * from a handler does NOT cause HelloBill to retry (a 2xx was already\n * written). Errors are logged via `opts.logger?.error`.\n */\nexport interface WebhookHandlers {\n onSessionEmbedOpened?(event: SessionEmbedOpened, ctx: WebhookHandlerContext): Promise<void>;\n onSessionDetailsConfirmed?(event: SessionDetailsConfirmed, ctx: WebhookHandlerContext): Promise<void>;\n onSessionProductsSelected?(event: SessionProductsSelected, ctx: WebhookHandlerContext): Promise<void>;\n onSessionLoaSigned?(event: SessionLoaSigned, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAccountCreated?(event: SessionAccountCreated, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAppInstalled?(event: SessionAppInstalled, ctx: WebhookHandlerContext): Promise<void>;\n onSessionAppDeferred?(event: SessionAppDeferred, ctx: WebhookHandlerContext): Promise<void>;\n onSubscriptionChanged?(event: SubscriptionChanged, ctx: WebhookHandlerContext): Promise<void>;\n onSetupStatusChanged?(event: SetupStatusChanged, ctx: WebhookHandlerContext): Promise<void>;\n onMoveOutNotificationSent?(event: MoveOutNotificationSent, ctx: WebhookHandlerContext): Promise<void>;\n onMoveOutNotificationFailed?(event: MoveOutNotificationFailed, ctx: WebhookHandlerContext): Promise<void>;\n onBankDetailsCollected?(event: BankDetailsCollected, ctx: WebhookHandlerContext): Promise<void>;\n}\n\n/** Options for the webhook route. Lives alongside `CreateHellobillHandlerOptions`. */\nexport interface WebhookHandlerOptions {\n /**\n * Partner webhook secret. From `process.env.HELLOBILL_WEBHOOK_SECRET`.\n * MUST be a non-empty string at `createWebhookHandler` call time — the\n * factory throws synchronously on `null`/`undefined`/`''` so that an\n * unset env var is caught immediately rather than silently 401ing every\n * incoming webhook.\n */\n webhookSecret: string;\n /**\n * Optional dedupe-store. Default: in-memory (single-process). Multi-replica\n * deployments MUST supply a shared backend (Redis, RDS, etc.) — see README.\n */\n dedupeStore?: WebhookDedupeStore;\n /**\n * Idempotency window in seconds. Default 86_400 (24h). Per spec §11.1 line 2270.\n */\n dedupeTtlSeconds?: number;\n /** Signature versions accepted. Default `['v1']`. Set to `['v1','v2']` during rotation. */\n supportedVersions?: VerifyWebhookOptions['supportedVersions'];\n /** Timestamp tolerance in seconds. Default 300. */\n tolerance?: number;\n /** Optional logger. Defaults to no-op. */\n logger?: Logger;\n /** Typed handlers — provide as many as you care about; missing ones log \"unhandled\". */\n handlers?: WebhookHandlers;\n /**\n * Test injection for `Date.now()`. Applied to the signature timestamp AND,\n * only when using the default `InMemoryWebhookDedupeStore`, the dedupe TTL\n * clock. Partner-supplied `dedupeStore` instances MUST own their own clock\n * injection — `opts.now` is NOT forwarded to a partner-supplied store.\n */\n now?: () => number;\n}\n\n/**\n * Thrown by `assertNever` when an event arrives whose `type` is not in the\n * known union — typically a newer platform event that this SDK version does\n * not yet handle. The `code` discriminator lets partners identify this class\n * of error in log aggregation without an `instanceof` check.\n */\nexport class UnknownWebhookEventError extends Error {\n readonly code = 'webhook.unknown_event_type' as const;\n\n constructor(eventType: string) {\n super(`Unhandled webhook event type: ${eventType}`);\n this.name = 'UnknownWebhookEventError';\n // Restore correct prototype chain in environments where extending Error\n // loses the prototype (pre-ES2015 transpilation targets).\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Sentinel — placed on the receiver's `default:` branch to surface compile-time exhaustiveness. */\nfunction assertNever(x: never): never {\n throw new UnknownWebhookEventError((x as { type?: string }).type ?? JSON.stringify(x));\n}\n\nconst noopLogger: Logger & { info: () => void } = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n};\n\n/**\n * Coalesces a partner-supplied Logger literal that may omit the optional `info`\n * method. Without this, `ctx.logger.info(...)` inside `logUnhandled` raises a\n * runtime TypeError when the partner's Logger only provides debug/warn/error.\n */\nfunction withInfoFallback(\n logger: Logger | undefined,\n): Logger & { info: (msg: string, meta?: Record<string, unknown>) => void } {\n return {\n debug: logger?.debug?.bind(logger) ?? (() => {}),\n info: logger?.info?.bind(logger) ?? (() => {}),\n warn: logger?.warn?.bind(logger) ?? (() => {}),\n error: logger?.error?.bind(logger) ?? (() => {}),\n };\n}\n\nfunction headerValue(\n headers: Record<string, string | string[] | undefined>,\n name: string,\n): string | undefined {\n const lower = name.toLowerCase();\n for (const [k, v] of Object.entries(headers)) {\n if (k.toLowerCase() === lower) return Array.isArray(v) ? v[0] : v;\n }\n return undefined;\n}\n\n/**\n * Dispatch a verified-and-deduped event to its typed handler. Exhaustive over\n * the union. Missing handler slots fall through to `logUnhandled` so a\n * partner-only-cares-about-bank-details deployment never 500s on an\n * unrecognised event type.\n */\nasync function dispatch(\n event: WebhookEvent,\n ctx: WebhookHandlerContext,\n handlers: WebhookHandlers,\n): Promise<void> {\n const logUnhandled = (name: string): void => {\n ctx.logger.info('hellobill.webhook.unhandled_variant', {\n event_id: event.id,\n event_type: event.type,\n handler: name,\n });\n };\n\n switch (event.type) {\n case 'session.embed_opened':\n if (handlers.onSessionEmbedOpened) await handlers.onSessionEmbedOpened(event, ctx);\n else logUnhandled('onSessionEmbedOpened');\n return;\n case 'session.details_confirmed':\n if (handlers.onSessionDetailsConfirmed) await handlers.onSessionDetailsConfirmed(event, ctx);\n else logUnhandled('onSessionDetailsConfirmed');\n return;\n case 'session.products_selected':\n if (handlers.onSessionProductsSelected) await handlers.onSessionProductsSelected(event, ctx);\n else logUnhandled('onSessionProductsSelected');\n return;\n case 'session.loa_signed':\n if (handlers.onSessionLoaSigned) await handlers.onSessionLoaSigned(event, ctx);\n else logUnhandled('onSessionLoaSigned');\n return;\n case 'session.account_created':\n if (handlers.onSessionAccountCreated) await handlers.onSessionAccountCreated(event, ctx);\n else logUnhandled('onSessionAccountCreated');\n return;\n case 'session.app_installed':\n if (handlers.onSessionAppInstalled) await handlers.onSessionAppInstalled(event, ctx);\n else logUnhandled('onSessionAppInstalled');\n return;\n case 'session.app_deferred':\n if (handlers.onSessionAppDeferred) await handlers.onSessionAppDeferred(event, ctx);\n else logUnhandled('onSessionAppDeferred');\n return;\n case 'subscription.changed':\n if (handlers.onSubscriptionChanged) await handlers.onSubscriptionChanged(event, ctx);\n else logUnhandled('onSubscriptionChanged');\n return;\n case 'setup_status.changed':\n if (handlers.onSetupStatusChanged) await handlers.onSetupStatusChanged(event, ctx);\n else logUnhandled('onSetupStatusChanged');\n return;\n case 'move_out_notification.sent':\n if (handlers.onMoveOutNotificationSent) await handlers.onMoveOutNotificationSent(event, ctx);\n else logUnhandled('onMoveOutNotificationSent');\n return;\n case 'move_out_notification.failed':\n if (handlers.onMoveOutNotificationFailed) await handlers.onMoveOutNotificationFailed(event, ctx);\n else logUnhandled('onMoveOutNotificationFailed');\n return;\n case 'bank_details.collected':\n if (handlers.onBankDetailsCollected) await handlers.onBankDetailsCollected(event, ctx);\n else logUnhandled('onBankDetailsCollected');\n return;\n default:\n // Exhaustiveness — if Ticket #1 adds a 13th variant, this fails to compile.\n return assertNever(event);\n }\n}\n\n/**\n * Creates the framework-agnostic webhook receive handler. Returns a function\n * matching the same `HellobillRequest`/`HellobillResponse` shape used by the\n * other routes on `HellobillHandlerSet` (see `core/handler.ts`).\n *\n * Throws synchronously if `opts.webhookSecret` is falsy — a missing or\n * empty secret is caught early so that every incoming webhook does not\n * silently 401 with no visible cause.\n *\n * The Express adapter wires this in alongside `session`/`loa`/etc.\n */\nexport function createWebhookHandler(\n opts: WebhookHandlerOptions,\n): (req: HellobillRequest, res: HellobillResponse) => Promise<void> {\n // Runtime guard: `process.env.HELLOBILL_WEBHOOK_SECRET!` bang-assertion satisfies\n // the TypeScript compiler but does not catch the case where the env var is unset at\n // runtime. Without this guard, every webhook would 401 silently because\n // verifyWebhookSignature would always return false for an undefined secret.\n if (opts.webhookSecret == null || opts.webhookSecret === '') {\n throw new Error(\n 'HELLOBILL_WEBHOOK_SECRET must be set; see spec §13.2 and ' +\n 'the @hello-bill/node README \"Webhook receiver\" section.',\n );\n }\n\n const now = opts.now ?? (() => Date.now());\n const dedupeStore = opts.dedupeStore ?? new InMemoryWebhookDedupeStore(now);\n const dedupeTtlSeconds = opts.dedupeTtlSeconds ?? 86_400;\n const tolerance = opts.tolerance ?? 300;\n const supportedVersions = opts.supportedVersions ?? ['v1'];\n const handlers = opts.handlers ?? {};\n // Coalesce partner-supplied Logger that may omit the optional `info` method.\n // Without this, `ctx.logger.info(...)` inside logUnhandled raises a runtime\n // TypeError when the partner Logger only provides debug/warn/error.\n const safeLogger = withInfoFallback(opts.logger ?? noopLogger);\n\n return async (req: HellobillRequest, res: HellobillResponse): Promise<void> => {\n const rawBody = req.rawBody;\n if (!rawBody) {\n safeLogger.error('hellobill.webhook.missing_raw_body', {});\n res.status(500).json({\n error: {\n type: 'api_error',\n code: 'server.internal',\n message: 'Webhook receiver requires raw body — adapter not wired correctly',\n },\n });\n return;\n }\n\n const sigHeader = headerValue(req.headers, 'x-hellobill-signature');\n if (!sigHeader) {\n res.status(401).json({ error: 'invalid_signature' });\n return;\n }\n\n const valid = verifyWebhookSignature(rawBody, sigHeader, opts.webhookSecret, {\n tolerance,\n supportedVersions,\n now,\n });\n if (!valid) {\n res.status(401).json({ error: 'invalid_signature' });\n return;\n }\n\n // Parse AFTER verification — verify operates on raw bytes by contract per spec §11.2.\n let event: WebhookEvent;\n try {\n event = JSON.parse(rawBody.toString('utf8')) as WebhookEvent;\n } catch {\n res.status(400).json({ error: 'invalid_payload' });\n return;\n }\n\n // Dedupe — record-before-dispatch so multi-replica double-delivery cannot\n // double-process. The crash-window caveat (rare loss) is documented and is\n // the at-most-once-after-2xx contract.\n const seen = await dedupeStore.has(event.id);\n if (seen) {\n safeLogger.info('hellobill.webhook.dedup_hit', { event_id: event.id, event_type: event.type });\n res.status(200).json({ received: true, deduped: true });\n return;\n }\n await dedupeStore.record(event.id, dedupeTtlSeconds);\n\n const deprecation = headerValue(req.headers, 'x-hellobill-deprecation') ?? null;\n if (deprecation) {\n safeLogger.warn('hellobill.webhook.deprecation_notice', {\n deprecation,\n event_id: event.id,\n event_type: event.type,\n });\n }\n\n // Respond 2xx FIRST. Per spec §11.1 line 2272: respond within 10s, process async.\n res.status(200).json({ received: true });\n\n const ctx: WebhookHandlerContext = {\n // safeLogger guarantees `info` is defined regardless of partner Logger shape.\n logger: safeLogger,\n headers: req.headers,\n deprecation,\n };\n\n // Fire-and-forget. Errors logged; HelloBill's retry layer is the durability mechanism.\n void dispatch(event, ctx, handlers).catch((err) => {\n safeLogger.error('hellobill.webhook.handler_error', {\n event_id: event.id,\n event_type: event.type,\n message: (err as Error).message,\n // Include the typed error code when present so partners can identify\n // known error classes (e.g. unknown event type) in log aggregation.\n ...(err != null && typeof err === 'object' && 'code' in err\n ? { code: (err as { code: unknown }).code }\n : {}),\n });\n });\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hello-bill/node",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Server-side SDK for the HelloBill Partner API. Framework-agnostic core with an Express adapter; ships a webhook verifier and the spec-derived TypeScript types.",
5
5
  "license": "MIT",
6
6
  "type": "module",