@hello-bill/node 1.0.3 → 2.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/dist/express/index.d.cts +2 -2
- package/dist/express/index.d.ts +2 -2
- package/dist/express/index.js +41 -6
- package/dist/express/index.js.map +1 -1
- package/dist/express/index.mjs +41 -6
- package/dist/express/index.mjs.map +1 -1
- package/dist/hono/index.d.cts +2 -2
- package/dist/hono/index.d.ts +2 -2
- package/dist/hono/index.js +40 -15
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/index.mjs +40 -15
- package/dist/hono/index.mjs.map +1 -1
- package/dist/{index-BrfG82zs.d.cts → index-D81ftBQo.d.ts} +16 -4
- package/dist/{index-EXetQn1f.d.ts → index-lPvEf5hZ.d.cts} +16 -4
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +26 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +26 -6
- package/dist/index.mjs.map +1 -1
- package/dist/koa/index.d.cts +2 -2
- package/dist/koa/index.d.ts +2 -2
- package/dist/koa/index.js +40 -14
- package/dist/koa/index.js.map +1 -1
- package/dist/koa/index.mjs +40 -14
- package/dist/koa/index.mjs.map +1 -1
- package/dist/next/index.d.cts +2 -2
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +31 -7
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +31 -7
- package/dist/next/index.mjs.map +1 -1
- package/dist/types/index.d.cts +3 -3
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/index.mjs.map +1 -1
- package/dist/webhook/index.d.cts +2 -2
- package/dist/webhook/index.d.ts +2 -2
- package/dist/webhook/index.js +14 -2
- package/dist/webhook/index.js.map +1 -1
- package/dist/webhook/index.mjs +14 -3
- package/dist/webhook/index.mjs.map +1 -1
- package/dist/{webhooks-DPpqf7d2.d.cts → webhooks-Cv4e8oXp.d.cts} +3 -2
- package/dist/{webhooks-DPpqf7d2.d.ts → webhooks-Cv4e8oXp.d.ts} +3 -2
- package/package.json +1 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { V as IsoDateTime, R as RequestId } from '../webhooks-
|
|
2
|
-
export { A as Address, i as AddressObject, j as AddressesObject, k as ApiVersion, l as AppDeeplinks, m as BankDetailsCollected, n as BankDetailsConsent, o as BankDetailsObject, B as BankDetailsRequest, h as BankDetailsResponse, p as BankDetailsStatus, q as BillType, r as BreakdownCoverProduct, s as BroadbandProduct, t as BuildingStyle, u as BuildingType, v as CacheStatus, w as ClientMode, x as Consent, y as ContextEntry, z as CouncilTaxProduct, C as CreateCustomerInput, D as Customer, e as CustomerCreateResponse, f as CustomerId, g as CustomerStatusResponse, E as CustomerType, F as DataCompleteness, G as DiscoveryStatus, H as Email, I as EnergyProduct, J as Environment, K as EpcRating, M as FinalRead, N as FuelType, O as HeatingType, Q as HomeInsuranceProduct, T as IdempotencyKey, U as IsoDate, W as LoASignature, X as LoaId, L as LoaListResponse, Y as LoaRecord, Z as LocationRole, _ as MaskedSortCode, $ as Meters, a0 as MobileProduct, a1 as MoveIn, a2 as MoveObject, a3 as MoveOut, a4 as MoveOutNotificationFailed, a5 as MoveOutNotificationSent, a6 as MoveOutNotifyFlags, a7 as MoveOutReason, a8 as MoveOutStatus, a9 as NearbyPoi, aa as OccupantObject, ab as OccupantType, ac as OnboardingInfo, ad as OverallStatus, ae as Paginated, af as Person, ag as PhoneNumber, ah as Postcode, ai as Product, aj as ProductBase, ak as ProductCategory, al as ProductConfidence, am as ProductId, an as ProductSetup, P as ProductsQuery, c as ProductsResponse, ao as PropertyDetailObject, d as PropertyDetailResponse, ap as PropertyLocation, aq as PropertyObject, ar as PropertyType, as as PropertyTypeCode, at as PropertyTypeSubcode, au as ProvidedFieldPath, av as SESSION_IDEMPOTENCY_TTL_MS, aw as SavingsSummaryResponse, ax as SelectedProduct, ay as SessionAccountCreated, az as SessionAppDeferred, aA as SessionAppInstalled, aB as SessionDetailsConfirmed, aC as SessionEmbedOpened, b as SessionGetResponse, aD as SessionLoaSigned, S as SessionPayload, aE as SessionProductsSelected, a as SessionResponse, aF as SetupStatus, aG as SetupStatusChanged, aH as SignatureImage, aI as StoredBankDetails, aJ as SubscriptionChanged, aK as SubscriptionStatus, aL as TvLicenceProduct, aM as Url, aN as WaterProduct, aO as WebhookEvent, aP as WebhookEventBase, aQ as WebhookEventId, aR as WebhookEventType } from '../webhooks-
|
|
1
|
+
import { V as IsoDateTime, R as RequestId } from '../webhooks-Cv4e8oXp.js';
|
|
2
|
+
export { A as Address, i as AddressObject, j as AddressesObject, k as ApiVersion, l as AppDeeplinks, m as BankDetailsCollected, n as BankDetailsConsent, o as BankDetailsObject, B as BankDetailsRequest, h as BankDetailsResponse, p as BankDetailsStatus, q as BillType, r as BreakdownCoverProduct, s as BroadbandProduct, t as BuildingStyle, u as BuildingType, v as CacheStatus, w as ClientMode, x as Consent, y as ContextEntry, z as CouncilTaxProduct, C as CreateCustomerInput, D as Customer, e as CustomerCreateResponse, f as CustomerId, g as CustomerStatusResponse, E as CustomerType, F as DataCompleteness, G as DiscoveryStatus, H as Email, I as EnergyProduct, J as Environment, K as EpcRating, M as FinalRead, N as FuelType, O as HeatingType, Q as HomeInsuranceProduct, T as IdempotencyKey, U as IsoDate, W as LoASignature, X as LoaId, L as LoaListResponse, Y as LoaRecord, Z as LocationRole, _ as MaskedSortCode, $ as Meters, a0 as MobileProduct, a1 as MoveIn, a2 as MoveObject, a3 as MoveOut, a4 as MoveOutNotificationFailed, a5 as MoveOutNotificationSent, a6 as MoveOutNotifyFlags, a7 as MoveOutReason, a8 as MoveOutStatus, a9 as NearbyPoi, aa as OccupantObject, ab as OccupantType, ac as OnboardingInfo, ad as OverallStatus, ae as Paginated, af as Person, ag as PhoneNumber, ah as Postcode, ai as Product, aj as ProductBase, ak as ProductCategory, al as ProductConfidence, am as ProductId, an as ProductSetup, P as ProductsQuery, c as ProductsResponse, ao as PropertyDetailObject, d as PropertyDetailResponse, ap as PropertyLocation, aq as PropertyObject, ar as PropertyType, as as PropertyTypeCode, at as PropertyTypeSubcode, au as ProvidedFieldPath, av as SESSION_IDEMPOTENCY_TTL_MS, aw as SavingsSummaryResponse, ax as SelectedProduct, ay as SessionAccountCreated, az as SessionAppDeferred, aA as SessionAppInstalled, aB as SessionDetailsConfirmed, aC as SessionEmbedOpened, b as SessionGetResponse, aD as SessionLoaSigned, S as SessionPayload, aE as SessionProductsSelected, a as SessionResponse, aF as SetupStatus, aG as SetupStatusChanged, aH as SignatureImage, aI as StoredBankDetails, aJ as SubscriptionChanged, aK as SubscriptionStatus, aL as TvLicenceProduct, aM as Url, aN as WaterProduct, aO as WebhookEvent, aP as WebhookEventBase, aQ as WebhookEventId, aR as WebhookEventType } from '../webhooks-Cv4e8oXp.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* AnyVan move slot types.
|
|
6
|
-
*
|
|
6
|
+
* Legacy AnyVan move slot types (superseded by service-slots schema).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
interface AnyvanSlot {
|
package/dist/types/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/types/session.ts","../../src/types/errors.ts"],"names":[],"mappings":";;;AAwFO,IAAM,0BAAA,GAA6B,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;;;ACRlD,IAAM,uBAAA,uBAA8B,GAAA,CAAY,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC","file":"index.js","sourcesContent":["/**\n * Session lifecycle types.\n * POST /partner/sessions creates a session and returns a short-lived session_token\n * that the embed presents to the frontend-facing endpoints.\n */\n\nimport type {\n AddressObject,\n Consent,\n ContextEntry,\n Customer,\n IsoDateTime,\n Meters,\n MoveIn,\n MoveOut,\n ProvidedFieldPath,\n OccupantObject,\n PropertyObject,\n} from './common.js';\n\nexport interface AddressesObject {\n /** The property the customer is moving INTO. Where utilities are set up. */\n current: PropertyObject;\n /** The property the customer is moving OUT of, if applicable. */\n previous?: PropertyObject;\n}\n\nexport interface MoveObject {\n in: MoveIn;\n out?: MoveOut;\n}\n\n/**\n * Canonical snake_case shape the SDK sends to POST /partner/sessions.\n * `buildSessionPayload(req)` on the server is responsible for assembling this\n * from whatever shape the partner's host page provides.\n */\nexport interface SessionPayload {\n /** Partner-controlled correlation. Echoed back on webhooks as `data.referral_id`. */\n referral_id?: string;\n customer: Customer;\n occupants?: OccupantObject[];\n addresses: AddressesObject;\n move: MoveObject;\n meters?: Meters;\n consent: Consent;\n context?: ContextEntry[];\n}\n\n/** Response from POST /partner/sessions. */\nexport interface SessionResponse {\n session_id: string;\n /** Short-lived JWT (300s) the embed presents to the SDK-mounted endpoints. */\n session_token: string;\n
|
|
1
|
+
{"version":3,"sources":["../../src/types/session.ts","../../src/types/errors.ts"],"names":[],"mappings":";;;AAwFO,IAAM,0BAAA,GAA6B,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;;;ACRlD,IAAM,uBAAA,uBAA8B,GAAA,CAAY,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC","file":"index.js","sourcesContent":["/**\n * Session lifecycle types.\n * POST /partner/sessions creates a session and returns a short-lived session_token\n * that the embed presents to the frontend-facing endpoints.\n */\n\nimport type {\n AddressObject,\n Consent,\n ContextEntry,\n Customer,\n IsoDateTime,\n Meters,\n MoveIn,\n MoveOut,\n ProvidedFieldPath,\n OccupantObject,\n PropertyObject,\n} from './common.js';\n\nexport interface AddressesObject {\n /** The property the customer is moving INTO. Where utilities are set up. */\n current: PropertyObject;\n /** The property the customer is moving OUT of, if applicable. */\n previous?: PropertyObject;\n}\n\nexport interface MoveObject {\n in: MoveIn;\n out?: MoveOut;\n}\n\n/**\n * Canonical snake_case shape the SDK sends to POST /partner/sessions.\n * `buildSessionPayload(req)` on the server is responsible for assembling this\n * from whatever shape the partner's host page provides.\n */\nexport interface SessionPayload {\n /** Partner-controlled correlation. Echoed back on webhooks as `data.referral_id`. */\n referral_id?: string;\n customer: Customer;\n occupants?: OccupantObject[];\n addresses: AddressesObject;\n move: MoveObject;\n meters?: Meters;\n consent: Consent;\n context?: ContextEntry[];\n}\n\n/** Response from POST /partner/sessions. */\nexport interface SessionResponse {\n session_id: string;\n /** Short-lived JWT (300s) the embed presents to the SDK-mounted endpoints. */\n session_token: string;\n embed_base_url: string;\n /** Initial discovery status. Embed polls /products until complete. */\n discovery_status: 'running' | 'complete';\n estimated_ready_seconds?: number;\n /** Dot-notation paths of fields HelloBill received. Drives adaptive UI. */\n provided_fields: ProvidedFieldPath[];\n /** ISO 8601. Session resource expires after 90 days. */\n expires_at: IsoDateTime;\n}\n\n/**\n * Response from GET /partner/sessions/:id.\n *\n * Per spec §3.7, the endpoint echoes back the full SessionPayload the partner\n * originally sent to POST /partner/sessions, extended with server-assigned fields.\n * This is distinct from SessionResponse (POST projection) which carries session_token\n * and embed_base_url but not the original payload fields.\n */\nexport interface SessionGetResponse extends SessionPayload {\n /** Stable server-assigned session identifier. */\n session_id: string;\n /** ISO 8601 timestamp when the session was created. */\n created_at: IsoDateTime;\n /** ISO 8601 session expiry (90 days from creation). */\n expires_at: IsoDateTime;\n /** Lifecycle status of the session. */\n status: 'created' | 'customer_created' | 'expired';\n}\n\n/**\n * Idempotency window for POST /sessions: 24h on\n * email + addresses.current.postcode + addresses.current.address_line_1.\n * `move.out` is NOT part of the key — partners cannot retroactively change shape.\n */\nexport const SESSION_IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1000;\n","/**\n * Spec error envelope.\n * Returned with non-2xx responses from the Partner API and surfaced verbatim by the SDK.\n */\n\nimport type { RequestId } from './common.js';\n\n/**\n * Error codes follow `<category>.<specific>` naming and match the v15 §12 catalog.\n */\nexport type ErrorCode =\n // validation.*\n | 'validation.missing_required_field'\n | 'validation.invalid_field_value'\n | 'validation.invalid_enum_value'\n // consent.*\n | 'consent.required'\n // product.*\n | 'product.id_expired'\n // loa.*\n | 'loa.coverage_missing'\n | 'loa.coverage_excess'\n | 'loa.session_mismatch'\n | 'loa.template_changed'\n | 'loa.selection_changed'\n // signature_image.*\n | 'signature_image.invalid'\n | 'signature_image.too_large'\n | 'signature_image.dimensions_invalid'\n // address.*\n | 'address.line_too_long'\n | 'address.postcode_invalid'\n // move_out.*\n | 'move_out.requires_previous_property'\n | 'move_out.invalid_date'\n // final_read.*\n | 'final_read.implausible'\n // customer.*\n | 'customer.address_invalid'\n | 'customer.not_found'\n // bank_details.*\n | 'bank_details.invalid_sort_code'\n | 'bank_details.invalid_account_number'\n | 'bank_details.modulus_check_failed'\n | 'bank_details.session_locked'\n // auth.*\n | 'auth.invalid_credentials'\n | 'auth.token_expired'\n | 'auth.token_malformed'\n | 'auth.insufficient_scope'\n // session.*\n | 'session.not_found'\n // rate.*\n | 'rate.limited'\n // internal.*\n | 'internal.error'\n // discovery.*\n | 'discovery.unavailable';\n\nexport type ErrorType =\n | 'authentication_error'\n | 'validation_error'\n | 'rate_limit_error'\n | 'api_error';\n\nexport interface ErrorEnvelope {\n error: {\n type: ErrorType;\n code: ErrorCode;\n message: string;\n /** Path of the offending field (e.g. \"addresses.current.postcode\"). */\n param?: string;\n /** For validation.missing_required_field — dot-notation list of missing fields. */\n missing_fields?: string[];\n /** Echo of `X-HelloBill-Request-Id` for support correlation. */\n request_id: RequestId;\n };\n}\n\n/** HTTP statuses the SDK retries automatically with exponential backoff. */\nexport const RETRYABLE_HTTP_STATUSES = new Set<number>([429, 500, 502, 503, 504]);\n"]}
|
package/dist/types/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/types/session.ts","../../src/types/errors.ts"],"names":[],"mappings":";AAwFO,IAAM,0BAAA,GAA6B,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;;;ACRlD,IAAM,uBAAA,uBAA8B,GAAA,CAAY,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC","file":"index.mjs","sourcesContent":["/**\n * Session lifecycle types.\n * POST /partner/sessions creates a session and returns a short-lived session_token\n * that the embed presents to the frontend-facing endpoints.\n */\n\nimport type {\n AddressObject,\n Consent,\n ContextEntry,\n Customer,\n IsoDateTime,\n Meters,\n MoveIn,\n MoveOut,\n ProvidedFieldPath,\n OccupantObject,\n PropertyObject,\n} from './common.js';\n\nexport interface AddressesObject {\n /** The property the customer is moving INTO. Where utilities are set up. */\n current: PropertyObject;\n /** The property the customer is moving OUT of, if applicable. */\n previous?: PropertyObject;\n}\n\nexport interface MoveObject {\n in: MoveIn;\n out?: MoveOut;\n}\n\n/**\n * Canonical snake_case shape the SDK sends to POST /partner/sessions.\n * `buildSessionPayload(req)` on the server is responsible for assembling this\n * from whatever shape the partner's host page provides.\n */\nexport interface SessionPayload {\n /** Partner-controlled correlation. Echoed back on webhooks as `data.referral_id`. */\n referral_id?: string;\n customer: Customer;\n occupants?: OccupantObject[];\n addresses: AddressesObject;\n move: MoveObject;\n meters?: Meters;\n consent: Consent;\n context?: ContextEntry[];\n}\n\n/** Response from POST /partner/sessions. */\nexport interface SessionResponse {\n session_id: string;\n /** Short-lived JWT (300s) the embed presents to the SDK-mounted endpoints. */\n session_token: string;\n
|
|
1
|
+
{"version":3,"sources":["../../src/types/session.ts","../../src/types/errors.ts"],"names":[],"mappings":";AAwFO,IAAM,0BAAA,GAA6B,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;;;ACRlD,IAAM,uBAAA,uBAA8B,GAAA,CAAY,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC","file":"index.mjs","sourcesContent":["/**\n * Session lifecycle types.\n * POST /partner/sessions creates a session and returns a short-lived session_token\n * that the embed presents to the frontend-facing endpoints.\n */\n\nimport type {\n AddressObject,\n Consent,\n ContextEntry,\n Customer,\n IsoDateTime,\n Meters,\n MoveIn,\n MoveOut,\n ProvidedFieldPath,\n OccupantObject,\n PropertyObject,\n} from './common.js';\n\nexport interface AddressesObject {\n /** The property the customer is moving INTO. Where utilities are set up. */\n current: PropertyObject;\n /** The property the customer is moving OUT of, if applicable. */\n previous?: PropertyObject;\n}\n\nexport interface MoveObject {\n in: MoveIn;\n out?: MoveOut;\n}\n\n/**\n * Canonical snake_case shape the SDK sends to POST /partner/sessions.\n * `buildSessionPayload(req)` on the server is responsible for assembling this\n * from whatever shape the partner's host page provides.\n */\nexport interface SessionPayload {\n /** Partner-controlled correlation. Echoed back on webhooks as `data.referral_id`. */\n referral_id?: string;\n customer: Customer;\n occupants?: OccupantObject[];\n addresses: AddressesObject;\n move: MoveObject;\n meters?: Meters;\n consent: Consent;\n context?: ContextEntry[];\n}\n\n/** Response from POST /partner/sessions. */\nexport interface SessionResponse {\n session_id: string;\n /** Short-lived JWT (300s) the embed presents to the SDK-mounted endpoints. */\n session_token: string;\n embed_base_url: string;\n /** Initial discovery status. Embed polls /products until complete. */\n discovery_status: 'running' | 'complete';\n estimated_ready_seconds?: number;\n /** Dot-notation paths of fields HelloBill received. Drives adaptive UI. */\n provided_fields: ProvidedFieldPath[];\n /** ISO 8601. Session resource expires after 90 days. */\n expires_at: IsoDateTime;\n}\n\n/**\n * Response from GET /partner/sessions/:id.\n *\n * Per spec §3.7, the endpoint echoes back the full SessionPayload the partner\n * originally sent to POST /partner/sessions, extended with server-assigned fields.\n * This is distinct from SessionResponse (POST projection) which carries session_token\n * and embed_base_url but not the original payload fields.\n */\nexport interface SessionGetResponse extends SessionPayload {\n /** Stable server-assigned session identifier. */\n session_id: string;\n /** ISO 8601 timestamp when the session was created. */\n created_at: IsoDateTime;\n /** ISO 8601 session expiry (90 days from creation). */\n expires_at: IsoDateTime;\n /** Lifecycle status of the session. */\n status: 'created' | 'customer_created' | 'expired';\n}\n\n/**\n * Idempotency window for POST /sessions: 24h on\n * email + addresses.current.postcode + addresses.current.address_line_1.\n * `move.out` is NOT part of the key — partners cannot retroactively change shape.\n */\nexport const SESSION_IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1000;\n","/**\n * Spec error envelope.\n * Returned with non-2xx responses from the Partner API and surfaced verbatim by the SDK.\n */\n\nimport type { RequestId } from './common.js';\n\n/**\n * Error codes follow `<category>.<specific>` naming and match the v15 §12 catalog.\n */\nexport type ErrorCode =\n // validation.*\n | 'validation.missing_required_field'\n | 'validation.invalid_field_value'\n | 'validation.invalid_enum_value'\n // consent.*\n | 'consent.required'\n // product.*\n | 'product.id_expired'\n // loa.*\n | 'loa.coverage_missing'\n | 'loa.coverage_excess'\n | 'loa.session_mismatch'\n | 'loa.template_changed'\n | 'loa.selection_changed'\n // signature_image.*\n | 'signature_image.invalid'\n | 'signature_image.too_large'\n | 'signature_image.dimensions_invalid'\n // address.*\n | 'address.line_too_long'\n | 'address.postcode_invalid'\n // move_out.*\n | 'move_out.requires_previous_property'\n | 'move_out.invalid_date'\n // final_read.*\n | 'final_read.implausible'\n // customer.*\n | 'customer.address_invalid'\n | 'customer.not_found'\n // bank_details.*\n | 'bank_details.invalid_sort_code'\n | 'bank_details.invalid_account_number'\n | 'bank_details.modulus_check_failed'\n | 'bank_details.session_locked'\n // auth.*\n | 'auth.invalid_credentials'\n | 'auth.token_expired'\n | 'auth.token_malformed'\n | 'auth.insufficient_scope'\n // session.*\n | 'session.not_found'\n // rate.*\n | 'rate.limited'\n // internal.*\n | 'internal.error'\n // discovery.*\n | 'discovery.unavailable';\n\nexport type ErrorType =\n | 'authentication_error'\n | 'validation_error'\n | 'rate_limit_error'\n | 'api_error';\n\nexport interface ErrorEnvelope {\n error: {\n type: ErrorType;\n code: ErrorCode;\n message: string;\n /** Path of the offending field (e.g. \"addresses.current.postcode\"). */\n param?: string;\n /** For validation.missing_required_field — dot-notation list of missing fields. */\n missing_fields?: string[];\n /** Echo of `X-HelloBill-Request-Id` for support correlation. */\n request_id: RequestId;\n };\n}\n\n/** HTTP statuses the SDK retries automatically with exponential backoff. */\nexport const RETRYABLE_HTTP_STATUSES = new Set<number>([429, 500, 502, 503, 504]);\n"]}
|
package/dist/webhook/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { c as InMemoryWebhookDedupeStore, V as VerifyWebhookOptions, W as WebhookDedupeStore, f as WebhookHandlerContext, g as WebhookHandlerOptions, h as WebhookHandlers, i as WebhookVerificationError, k as createWebhookHandler, v as verifyWebhook, l as verifyWebhookSignature } from '../index-
|
|
2
|
-
import '../webhooks-
|
|
1
|
+
export { c as InMemoryWebhookDedupeStore, U as UnknownWebhookEventError, V as VerifyWebhookOptions, W as WebhookDedupeStore, f as WebhookHandlerContext, g as WebhookHandlerOptions, h as WebhookHandlers, i as WebhookVerificationError, k as createWebhookHandler, v as verifyWebhook, l as verifyWebhookSignature } from '../index-lPvEf5hZ.cjs';
|
|
2
|
+
import '../webhooks-Cv4e8oXp.cjs';
|
package/dist/webhook/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { c as InMemoryWebhookDedupeStore, V as VerifyWebhookOptions, W as WebhookDedupeStore, f as WebhookHandlerContext, g as WebhookHandlerOptions, h as WebhookHandlers, i as WebhookVerificationError, k as createWebhookHandler, v as verifyWebhook, l as verifyWebhookSignature } from '../index-
|
|
2
|
-
import '../webhooks-
|
|
1
|
+
export { c as InMemoryWebhookDedupeStore, U as UnknownWebhookEventError, V as VerifyWebhookOptions, W as WebhookDedupeStore, f as WebhookHandlerContext, g as WebhookHandlerOptions, h as WebhookHandlers, i as WebhookVerificationError, k as createWebhookHandler, v as verifyWebhook, l as verifyWebhookSignature } from '../index-D81ftBQo.js';
|
|
2
|
+
import '../webhooks-Cv4e8oXp.js';
|
package/dist/webhook/index.js
CHANGED
|
@@ -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
|
|
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"]}
|
package/dist/webhook/index.mjs
CHANGED
|
@@ -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
|
|
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"]}
|
|
@@ -247,7 +247,7 @@ interface SessionResponse {
|
|
|
247
247
|
session_id: string;
|
|
248
248
|
/** Short-lived JWT (300s) the embed presents to the SDK-mounted endpoints. */
|
|
249
249
|
session_token: string;
|
|
250
|
-
|
|
250
|
+
embed_base_url: string;
|
|
251
251
|
/** Initial discovery status. Embed polls /products until complete. */
|
|
252
252
|
discovery_status: 'running' | 'complete';
|
|
253
253
|
estimated_ready_seconds?: number;
|
|
@@ -262,7 +262,7 @@ interface SessionResponse {
|
|
|
262
262
|
* Per spec §3.7, the endpoint echoes back the full SessionPayload the partner
|
|
263
263
|
* originally sent to POST /partner/sessions, extended with server-assigned fields.
|
|
264
264
|
* This is distinct from SessionResponse (POST projection) which carries session_token
|
|
265
|
-
* and
|
|
265
|
+
* and embed_base_url but not the original payload fields.
|
|
266
266
|
*/
|
|
267
267
|
interface SessionGetResponse extends SessionPayload {
|
|
268
268
|
/** Stable server-assigned session identifier. */
|
|
@@ -638,6 +638,7 @@ interface CustomerStatusResponse {
|
|
|
638
638
|
move_out_status: MoveOutStatus;
|
|
639
639
|
subscription_status: SubscriptionStatus;
|
|
640
640
|
trial_ends_at: IsoDateTime | null;
|
|
641
|
+
savings_summary: SavingsSummaryResponse | null;
|
|
641
642
|
products: Array<ProductSetup & {
|
|
642
643
|
last_updated_at: IsoDateTime;
|
|
643
644
|
}>;
|
|
@@ -247,7 +247,7 @@ interface SessionResponse {
|
|
|
247
247
|
session_id: string;
|
|
248
248
|
/** Short-lived JWT (300s) the embed presents to the SDK-mounted endpoints. */
|
|
249
249
|
session_token: string;
|
|
250
|
-
|
|
250
|
+
embed_base_url: string;
|
|
251
251
|
/** Initial discovery status. Embed polls /products until complete. */
|
|
252
252
|
discovery_status: 'running' | 'complete';
|
|
253
253
|
estimated_ready_seconds?: number;
|
|
@@ -262,7 +262,7 @@ interface SessionResponse {
|
|
|
262
262
|
* Per spec §3.7, the endpoint echoes back the full SessionPayload the partner
|
|
263
263
|
* originally sent to POST /partner/sessions, extended with server-assigned fields.
|
|
264
264
|
* This is distinct from SessionResponse (POST projection) which carries session_token
|
|
265
|
-
* and
|
|
265
|
+
* and embed_base_url but not the original payload fields.
|
|
266
266
|
*/
|
|
267
267
|
interface SessionGetResponse extends SessionPayload {
|
|
268
268
|
/** Stable server-assigned session identifier. */
|
|
@@ -638,6 +638,7 @@ interface CustomerStatusResponse {
|
|
|
638
638
|
move_out_status: MoveOutStatus;
|
|
639
639
|
subscription_status: SubscriptionStatus;
|
|
640
640
|
trial_ends_at: IsoDateTime | null;
|
|
641
|
+
savings_summary: SavingsSummaryResponse | null;
|
|
641
642
|
products: Array<ProductSetup & {
|
|
642
643
|
last_updated_at: IsoDateTime;
|
|
643
644
|
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hello-bill/node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|