@hello-bill/node 1.0.0 → 1.0.1

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +216 -0
  4. package/dist/express/index.d.cts +34 -0
  5. package/dist/express/index.d.ts +34 -0
  6. package/dist/express/index.js +1019 -0
  7. package/dist/express/index.js.map +1 -0
  8. package/dist/express/index.mjs +1016 -0
  9. package/dist/express/index.mjs.map +1 -0
  10. package/dist/hono/index.d.cts +37 -0
  11. package/dist/hono/index.d.ts +37 -0
  12. package/dist/hono/index.js +1036 -0
  13. package/dist/hono/index.js.map +1 -0
  14. package/dist/hono/index.mjs +1033 -0
  15. package/dist/hono/index.mjs.map +1 -0
  16. package/dist/index-BrfG82zs.d.cts +341 -0
  17. package/dist/index-EXetQn1f.d.ts +341 -0
  18. package/dist/index.d.cts +164 -0
  19. package/dist/index.d.ts +164 -0
  20. package/dist/index.js +1178 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/index.mjs +1164 -0
  23. package/dist/index.mjs.map +1 -0
  24. package/dist/koa/index.d.cts +42 -0
  25. package/dist/koa/index.d.ts +42 -0
  26. package/dist/koa/index.js +1042 -0
  27. package/dist/koa/index.js.map +1 -0
  28. package/dist/koa/index.mjs +1039 -0
  29. package/dist/koa/index.mjs.map +1 -0
  30. package/dist/next/index.d.cts +60 -0
  31. package/dist/next/index.d.ts +60 -0
  32. package/dist/next/index.js +1092 -0
  33. package/dist/next/index.js.map +1 -0
  34. package/dist/next/index.mjs +1090 -0
  35. package/dist/next/index.mjs.map +1 -0
  36. package/dist/types/index.d.cts +51 -0
  37. package/dist/types/index.d.ts +51 -0
  38. package/dist/types/index.js +12 -0
  39. package/dist/types/index.js.map +1 -0
  40. package/dist/types/index.mjs +9 -0
  41. package/dist/types/index.mjs.map +1 -0
  42. package/dist/webhook/index.d.cts +2 -0
  43. package/dist/webhook/index.d.ts +2 -0
  44. package/dist/webhook/index.js +342 -0
  45. package/dist/webhook/index.js.map +1 -0
  46. package/dist/webhook/index.mjs +336 -0
  47. package/dist/webhook/index.mjs.map +1 -0
  48. package/dist/webhooks-DPpqf7d2.d.cts +751 -0
  49. package/dist/webhooks-DPpqf7d2.d.ts +751 -0
  50. package/package.json +164 -7
package/dist/index.js ADDED
@@ -0,0 +1,1178 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+
5
+ // src/types/session.ts
6
+
7
+ // src/types/errors.ts
8
+ var RETRYABLE_HTTP_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
9
+
10
+ // src/core/retry.ts
11
+ var NetworkError = class extends Error {
12
+ constructor(message, cause, kind = "network") {
13
+ super(message);
14
+ this.cause = cause;
15
+ this.kind = kind;
16
+ this.name = "NetworkError";
17
+ }
18
+ };
19
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
+ function parseRetryAfter(value) {
21
+ if (!value) return null;
22
+ const seconds = Number(value);
23
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
24
+ return null;
25
+ }
26
+ function backoff(attempt, base, cap, random) {
27
+ const exp = Math.min(cap, base * 2 ** attempt);
28
+ return Math.floor(random() * exp);
29
+ }
30
+ async function readErrorCode(res) {
31
+ try {
32
+ const clone = res.clone();
33
+ const body = await clone.json();
34
+ return body?.error?.code;
35
+ } catch {
36
+ return void 0;
37
+ }
38
+ }
39
+ async function retryFetch(input, init, opts = {}) {
40
+ const {
41
+ maxAttempts = 5,
42
+ baseDelayMs = 1e3,
43
+ maxDelayMs = 16e3,
44
+ fetch: fetchImpl = globalThis.fetch,
45
+ sleep = defaultSleep,
46
+ random = Math.random,
47
+ onTokenExpired,
48
+ timeoutMs = 3e4
49
+ } = opts;
50
+ let tokenRefreshed = false;
51
+ let lastResponse;
52
+ let lastError;
53
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
54
+ let res;
55
+ try {
56
+ res = await fetchWithTimeout(fetchImpl, input, init, timeoutMs);
57
+ } catch (err) {
58
+ lastError = err;
59
+ if (attempt === maxAttempts - 1) {
60
+ if (err instanceof NetworkError) throw err;
61
+ throw new NetworkError(
62
+ "upstream request failed",
63
+ err,
64
+ isAbortLike(err) ? "timeout" : "network"
65
+ );
66
+ }
67
+ const delay2 = backoff(attempt, baseDelayMs, maxDelayMs, random);
68
+ await sleep(delay2);
69
+ continue;
70
+ }
71
+ lastResponse = res;
72
+ if (res.ok) return res;
73
+ if (!tokenRefreshed && onTokenExpired && res.status === 401) {
74
+ const code = await readErrorCode(res);
75
+ if (code === "auth.token_expired") {
76
+ tokenRefreshed = true;
77
+ await onTokenExpired();
78
+ continue;
79
+ }
80
+ }
81
+ if (!RETRYABLE_HTTP_STATUSES.has(res.status)) return res;
82
+ if (attempt === maxAttempts - 1) return res;
83
+ const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
84
+ const delay = retryAfterMs ?? backoff(attempt, baseDelayMs, maxDelayMs, random);
85
+ await sleep(delay);
86
+ }
87
+ if (lastResponse) return lastResponse;
88
+ throw new NetworkError("upstream request failed", lastError);
89
+ }
90
+ function isAbortLike(err) {
91
+ if (!err || typeof err !== "object") return false;
92
+ const name = err.name;
93
+ return name === "AbortError" || name === "TimeoutError";
94
+ }
95
+ async function fetchWithTimeout(fetchImpl, input, init, timeoutMs) {
96
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
97
+ return fetchImpl(input, init);
98
+ }
99
+ const controller = new AbortController();
100
+ const userSignal = init.signal;
101
+ if (userSignal) {
102
+ if (userSignal.aborted) controller.abort(userSignal.reason);
103
+ else userSignal.addEventListener("abort", () => controller.abort(userSignal.reason), { once: true });
104
+ }
105
+ const timer = setTimeout(() => controller.abort(new Error("upstream timeout")), timeoutMs);
106
+ try {
107
+ return await fetchImpl(input, { ...init, signal: controller.signal });
108
+ } catch (err) {
109
+ if (controller.signal.aborted && !userSignal?.aborted) {
110
+ throw new NetworkError(`upstream request timed out after ${timeoutMs}ms`, err, "timeout");
111
+ }
112
+ throw err;
113
+ } finally {
114
+ clearTimeout(timer);
115
+ }
116
+ }
117
+
118
+ // src/core/client.ts
119
+ function buildQueryString(query) {
120
+ if (!query) return "";
121
+ const sp = new URLSearchParams();
122
+ for (const [k, v] of Object.entries(query)) {
123
+ if (v === void 0) continue;
124
+ if (Array.isArray(v)) {
125
+ for (const item of v) sp.append(k, item);
126
+ } else {
127
+ sp.append(k, v);
128
+ }
129
+ }
130
+ const qs = sp.toString();
131
+ return qs ? `?${qs}` : "";
132
+ }
133
+ async function callUpstream(apiBaseUrl, tokenManager, opts, fetchImpl) {
134
+ const base = apiBaseUrl.replace(/\/$/, "");
135
+ const url = `${base}${opts.path}${buildQueryString(opts.query)}`;
136
+ const token = await tokenManager.getAccessToken();
137
+ const headers = {
138
+ Authorization: `Bearer ${token}`,
139
+ Accept: "application/json"
140
+ };
141
+ if (opts.body !== void 0) headers["Content-Type"] = "application/json";
142
+ if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
143
+ const init = {
144
+ method: opts.method,
145
+ headers
146
+ };
147
+ if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
148
+ const res = await retryFetch(url, init, {
149
+ fetch: fetchImpl,
150
+ timeoutMs: opts.timeoutMs,
151
+ onTokenExpired: async () => {
152
+ await tokenManager.invalidate();
153
+ const fresh = await tokenManager.getAccessToken();
154
+ headers.Authorization = `Bearer ${fresh}`;
155
+ }
156
+ });
157
+ let body = null;
158
+ const text = await res.text();
159
+ if (text.length > 0) {
160
+ try {
161
+ body = JSON.parse(text);
162
+ } catch {
163
+ body = text;
164
+ }
165
+ }
166
+ return {
167
+ status: res.status,
168
+ headers: res.headers,
169
+ body,
170
+ requestId: res.headers.get("x-hellobill-request-id") ?? void 0
171
+ };
172
+ }
173
+ function defaultIdempotencyKey() {
174
+ return crypto.randomUUID();
175
+ }
176
+
177
+ // src/core/errors.ts
178
+ var HellobillApiError = class extends Error {
179
+ status;
180
+ envelope;
181
+ requestId;
182
+ headers;
183
+ constructor(status, envelope, requestId, headers) {
184
+ super(envelope?.error.message ?? `HelloBill API error (${status})`);
185
+ this.name = "HellobillApiError";
186
+ this.status = status;
187
+ this.envelope = envelope;
188
+ this.requestId = requestId;
189
+ this.headers = headers;
190
+ }
191
+ get code() {
192
+ return this.envelope?.error.code;
193
+ }
194
+ };
195
+
196
+ // src/core/token-manager.ts
197
+ var InMemoryTokenStorage = class {
198
+ map = /* @__PURE__ */ new Map();
199
+ async get(clientId) {
200
+ return this.map.get(clientId) ?? null;
201
+ }
202
+ async set(clientId, value) {
203
+ this.map.set(clientId, value);
204
+ }
205
+ };
206
+ var TokenManager = class {
207
+ clientId;
208
+ clientSecret;
209
+ apiBaseUrl;
210
+ fetchImpl;
211
+ storage;
212
+ logger;
213
+ refreshLeewayMs;
214
+ now;
215
+ inflight = null;
216
+ constructor(opts) {
217
+ this.clientId = opts.clientId;
218
+ this.clientSecret = opts.clientSecret;
219
+ this.apiBaseUrl = opts.apiBaseUrl.replace(/\/$/, "");
220
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
221
+ this.storage = opts.storage ?? new InMemoryTokenStorage();
222
+ this.logger = opts.logger;
223
+ this.refreshLeewayMs = opts.refreshLeewayMs ?? 3e4;
224
+ this.now = opts.now ?? Date.now;
225
+ }
226
+ /** Returns a valid bearer token, refreshing if necessary. Single-flight on refresh. */
227
+ async getAccessToken() {
228
+ if (this.inflight) return this.inflight;
229
+ const cached = await this.storage.get(this.clientId);
230
+ if (cached && cached.accessToken && cached.expiresAt - this.refreshLeewayMs > this.now()) {
231
+ return cached.accessToken;
232
+ }
233
+ if (this.inflight) return this.inflight;
234
+ this.inflight = this.fetchToken().finally(() => {
235
+ this.inflight = null;
236
+ });
237
+ return this.inflight;
238
+ }
239
+ /** Forces invalidation of the cached token. Next `getAccessToken` will refresh. */
240
+ async invalidate() {
241
+ await this.storage.set(this.clientId, { accessToken: "", expiresAt: 0 });
242
+ }
243
+ async fetchToken() {
244
+ const body = new URLSearchParams({
245
+ grant_type: "client_credentials",
246
+ client_id: this.clientId,
247
+ client_secret: this.clientSecret
248
+ });
249
+ const res = await this.fetchImpl(`${this.apiBaseUrl}/auth/partner/token`, {
250
+ method: "POST",
251
+ headers: {
252
+ "Content-Type": "application/x-www-form-urlencoded",
253
+ Accept: "application/json"
254
+ },
255
+ body: body.toString()
256
+ });
257
+ if (!res.ok) {
258
+ let envelope = null;
259
+ try {
260
+ envelope = await res.json();
261
+ } catch {
262
+ }
263
+ const requestId = res.headers.get("x-hellobill-request-id") ?? void 0;
264
+ this.logger?.error("hellobill.token_refresh_failed", { status: res.status });
265
+ throw new HellobillApiError(res.status, envelope, requestId, res.headers);
266
+ }
267
+ const json = await res.json();
268
+ const expiresAt = this.now() + json.expires_in * 1e3;
269
+ await this.storage.set(this.clientId, {
270
+ accessToken: json.access_token,
271
+ expiresAt
272
+ });
273
+ this.logger?.debug("hellobill.token_refreshed", { expires_in: json.expires_in });
274
+ return json.access_token;
275
+ }
276
+ };
277
+ var WebhookVerificationError = class extends Error {
278
+ constructor(message, code) {
279
+ super(message);
280
+ this.code = code;
281
+ this.name = "WebhookVerificationError";
282
+ }
283
+ };
284
+ function parseHeader(signatureHeader) {
285
+ if (!signatureHeader || typeof signatureHeader !== "string") {
286
+ throw new WebhookVerificationError(
287
+ "Missing signature header",
288
+ "signature.missing_header"
289
+ );
290
+ }
291
+ const parts = signatureHeader.split(",").map((p) => p.trim()).filter(Boolean);
292
+ let t;
293
+ const signatures = {};
294
+ for (const part of parts) {
295
+ const eq = part.indexOf("=");
296
+ if (eq === -1) {
297
+ throw new WebhookVerificationError(
298
+ "Malformed signature header element",
299
+ "signature.malformed"
300
+ );
301
+ }
302
+ const key = part.slice(0, eq);
303
+ const value = part.slice(eq + 1);
304
+ if (key === "t") {
305
+ t = Number(value);
306
+ if (!Number.isFinite(t)) {
307
+ throw new WebhookVerificationError(
308
+ "Malformed timestamp in signature header",
309
+ "signature.malformed"
310
+ );
311
+ }
312
+ } else if (key.length > 0 && value.length > 0) {
313
+ signatures[key] = value;
314
+ }
315
+ }
316
+ if (t === void 0) {
317
+ throw new WebhookVerificationError(
318
+ "Missing timestamp in signature header",
319
+ "signature.malformed"
320
+ );
321
+ }
322
+ if (Object.keys(signatures).length === 0) {
323
+ throw new WebhookVerificationError(
324
+ "No signature values in header",
325
+ "signature.malformed"
326
+ );
327
+ }
328
+ return { t, signatures };
329
+ }
330
+ function computeHmacHex(secret, payload) {
331
+ return crypto.createHmac("sha256", secret).update(payload).digest("hex");
332
+ }
333
+ function constantTimeHexEqual(a, b) {
334
+ if (a.length !== b.length) return false;
335
+ try {
336
+ return crypto.timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
337
+ } catch {
338
+ return false;
339
+ }
340
+ }
341
+ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
342
+ const tolerance = opts.tolerance ?? 300;
343
+ const supportedVersions = opts.supportedVersions ?? ["v1"];
344
+ const now = opts.now ?? Date.now;
345
+ const { t, signatures } = parseHeader(signatureHeader);
346
+ const nowSeconds = Math.floor(now() / 1e3);
347
+ const skew = Math.abs(nowSeconds - t);
348
+ if (skew > tolerance) {
349
+ throw new WebhookVerificationError(
350
+ `Signature timestamp outside tolerance (${skew}s > ${tolerance}s)`,
351
+ "signature.expired"
352
+ );
353
+ }
354
+ const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);
355
+ if (!hasAnyKnownVersion) {
356
+ throw new WebhookVerificationError(
357
+ "No supported signature version present in header",
358
+ "signature.unsupported_version"
359
+ );
360
+ }
361
+ const bodyStr = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
362
+ const signingInput = `${t}.${bodyStr}`;
363
+ for (const version of supportedVersions) {
364
+ const provided = signatures[version];
365
+ if (!provided) continue;
366
+ const expected = computeHmacHex(secret, signingInput);
367
+ if (constantTimeHexEqual(expected, provided)) {
368
+ try {
369
+ return JSON.parse(bodyStr);
370
+ } catch {
371
+ throw new WebhookVerificationError(
372
+ "Verified payload is not valid JSON",
373
+ "signature.malformed"
374
+ );
375
+ }
376
+ }
377
+ }
378
+ throw new WebhookVerificationError(
379
+ "Signature does not match expected value",
380
+ "signature.invalid"
381
+ );
382
+ }
383
+ function verifyWebhookSignature(rawBody, signatureHeader, secret, opts = {}) {
384
+ const tolerance = opts.tolerance ?? 300;
385
+ const supportedVersions = opts.supportedVersions ?? ["v1"];
386
+ const now = opts.now ?? Date.now;
387
+ let parsed;
388
+ try {
389
+ parsed = parseHeader(signatureHeader);
390
+ } catch (err) {
391
+ if (err instanceof WebhookVerificationError) return false;
392
+ throw err;
393
+ }
394
+ const { t, signatures } = parsed;
395
+ const nowSeconds = Math.floor(now() / 1e3);
396
+ const skew = Math.abs(nowSeconds - t);
397
+ if (skew > tolerance) return false;
398
+ const hasAnyKnownVersion = supportedVersions.some((v) => v in signatures);
399
+ if (!hasAnyKnownVersion) return false;
400
+ const bodyStr = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
401
+ const signingInput = `${t}.${bodyStr}`;
402
+ for (const version of supportedVersions) {
403
+ const provided = signatures[version];
404
+ if (!provided) continue;
405
+ const expected = computeHmacHex(secret, signingInput);
406
+ if (constantTimeHexEqual(expected, provided)) {
407
+ return true;
408
+ }
409
+ }
410
+ return false;
411
+ }
412
+
413
+ // src/webhook/dedupe-store.ts
414
+ var InMemoryWebhookDedupeStore = class {
415
+ // Optional test injection. Defaults to Date.now.
416
+ constructor(now = () => Date.now()) {
417
+ this.now = now;
418
+ }
419
+ store = /* @__PURE__ */ new Map();
420
+ async has(id) {
421
+ const expiresAt = this.store.get(id);
422
+ if (expiresAt === void 0) return false;
423
+ if (expiresAt <= this.now()) {
424
+ this.store.delete(id);
425
+ return false;
426
+ }
427
+ return true;
428
+ }
429
+ async record(id, ttlSeconds) {
430
+ this.store.set(id, this.now() + ttlSeconds * 1e3);
431
+ }
432
+ };
433
+
434
+ // src/webhook/handler.ts
435
+ function assertNever(x) {
436
+ throw new Error(`Unhandled webhook variant: ${JSON.stringify(x)}`);
437
+ }
438
+ var noopLogger = {
439
+ debug: () => {
440
+ },
441
+ info: () => {
442
+ },
443
+ warn: () => {
444
+ },
445
+ error: () => {
446
+ }
447
+ };
448
+ function withInfoFallback(logger) {
449
+ return {
450
+ debug: logger?.debug?.bind(logger) ?? (() => {
451
+ }),
452
+ info: logger?.info?.bind(logger) ?? (() => {
453
+ }),
454
+ warn: logger?.warn?.bind(logger) ?? (() => {
455
+ }),
456
+ error: logger?.error?.bind(logger) ?? (() => {
457
+ })
458
+ };
459
+ }
460
+ function headerValue(headers, name) {
461
+ const lower = name.toLowerCase();
462
+ for (const [k, v] of Object.entries(headers)) {
463
+ if (k.toLowerCase() === lower) return Array.isArray(v) ? v[0] : v;
464
+ }
465
+ return void 0;
466
+ }
467
+ async function dispatch(event, ctx, handlers) {
468
+ const logUnhandled = (name) => {
469
+ ctx.logger.info("hellobill.webhook.unhandled_variant", {
470
+ event_id: event.id,
471
+ event_type: event.type,
472
+ handler: name
473
+ });
474
+ };
475
+ switch (event.type) {
476
+ case "session.embed_opened":
477
+ if (handlers.onSessionEmbedOpened) await handlers.onSessionEmbedOpened(event, ctx);
478
+ else logUnhandled("onSessionEmbedOpened");
479
+ return;
480
+ case "session.details_confirmed":
481
+ if (handlers.onSessionDetailsConfirmed) await handlers.onSessionDetailsConfirmed(event, ctx);
482
+ else logUnhandled("onSessionDetailsConfirmed");
483
+ return;
484
+ case "session.products_selected":
485
+ if (handlers.onSessionProductsSelected) await handlers.onSessionProductsSelected(event, ctx);
486
+ else logUnhandled("onSessionProductsSelected");
487
+ return;
488
+ case "session.loa_signed":
489
+ if (handlers.onSessionLoaSigned) await handlers.onSessionLoaSigned(event, ctx);
490
+ else logUnhandled("onSessionLoaSigned");
491
+ return;
492
+ case "session.account_created":
493
+ if (handlers.onSessionAccountCreated) await handlers.onSessionAccountCreated(event, ctx);
494
+ else logUnhandled("onSessionAccountCreated");
495
+ return;
496
+ case "session.app_installed":
497
+ if (handlers.onSessionAppInstalled) await handlers.onSessionAppInstalled(event, ctx);
498
+ else logUnhandled("onSessionAppInstalled");
499
+ return;
500
+ case "session.app_deferred":
501
+ if (handlers.onSessionAppDeferred) await handlers.onSessionAppDeferred(event, ctx);
502
+ else logUnhandled("onSessionAppDeferred");
503
+ return;
504
+ case "subscription.changed":
505
+ if (handlers.onSubscriptionChanged) await handlers.onSubscriptionChanged(event, ctx);
506
+ else logUnhandled("onSubscriptionChanged");
507
+ return;
508
+ case "setup_status.changed":
509
+ if (handlers.onSetupStatusChanged) await handlers.onSetupStatusChanged(event, ctx);
510
+ else logUnhandled("onSetupStatusChanged");
511
+ return;
512
+ case "move_out_notification.sent":
513
+ if (handlers.onMoveOutNotificationSent) await handlers.onMoveOutNotificationSent(event, ctx);
514
+ else logUnhandled("onMoveOutNotificationSent");
515
+ return;
516
+ case "move_out_notification.failed":
517
+ if (handlers.onMoveOutNotificationFailed) await handlers.onMoveOutNotificationFailed(event, ctx);
518
+ else logUnhandled("onMoveOutNotificationFailed");
519
+ return;
520
+ case "bank_details.collected":
521
+ if (handlers.onBankDetailsCollected) await handlers.onBankDetailsCollected(event, ctx);
522
+ else logUnhandled("onBankDetailsCollected");
523
+ return;
524
+ default:
525
+ return assertNever(event);
526
+ }
527
+ }
528
+ function createWebhookHandler(opts) {
529
+ if (opts.webhookSecret == null || opts.webhookSecret === "") {
530
+ throw new Error(
531
+ 'HELLOBILL_WEBHOOK_SECRET must be set; see spec \xA713.2 and the @hello-bill/node README "Webhook receiver" section.'
532
+ );
533
+ }
534
+ const now = opts.now ?? (() => Date.now());
535
+ const dedupeStore = opts.dedupeStore ?? new InMemoryWebhookDedupeStore(now);
536
+ const dedupeTtlSeconds = opts.dedupeTtlSeconds ?? 86400;
537
+ const tolerance = opts.tolerance ?? 300;
538
+ const supportedVersions = opts.supportedVersions ?? ["v1"];
539
+ const handlers = opts.handlers ?? {};
540
+ const safeLogger = withInfoFallback(opts.logger ?? noopLogger);
541
+ return async (req, res) => {
542
+ const rawBody = req.rawBody;
543
+ if (!rawBody) {
544
+ safeLogger.error("hellobill.webhook.missing_raw_body", {});
545
+ res.status(500).json({
546
+ error: {
547
+ type: "api_error",
548
+ code: "server.internal",
549
+ message: "Webhook receiver requires raw body \u2014 adapter not wired correctly"
550
+ }
551
+ });
552
+ return;
553
+ }
554
+ const sigHeader = headerValue(req.headers, "x-hellobill-signature");
555
+ if (!sigHeader) {
556
+ res.status(401).json({ error: "invalid_signature" });
557
+ return;
558
+ }
559
+ const valid = verifyWebhookSignature(rawBody, sigHeader, opts.webhookSecret, {
560
+ tolerance,
561
+ supportedVersions,
562
+ now
563
+ });
564
+ if (!valid) {
565
+ res.status(401).json({ error: "invalid_signature" });
566
+ return;
567
+ }
568
+ let event;
569
+ try {
570
+ event = JSON.parse(rawBody.toString("utf8"));
571
+ } catch {
572
+ res.status(400).json({ error: "invalid_payload" });
573
+ return;
574
+ }
575
+ const seen = await dedupeStore.has(event.id);
576
+ if (seen) {
577
+ safeLogger.info("hellobill.webhook.dedup_hit", { event_id: event.id, event_type: event.type });
578
+ res.status(200).json({ received: true, deduped: true });
579
+ return;
580
+ }
581
+ await dedupeStore.record(event.id, dedupeTtlSeconds);
582
+ const deprecation = headerValue(req.headers, "x-hellobill-deprecation") ?? null;
583
+ if (deprecation) {
584
+ safeLogger.warn("hellobill.webhook.deprecation_notice", {
585
+ deprecation,
586
+ event_id: event.id,
587
+ event_type: event.type
588
+ });
589
+ }
590
+ res.status(200).json({ received: true });
591
+ const ctx = {
592
+ // safeLogger guarantees `info` is defined regardless of partner Logger shape.
593
+ logger: safeLogger,
594
+ headers: req.headers,
595
+ deprecation
596
+ };
597
+ void dispatch(event, ctx, handlers).catch((err) => {
598
+ safeLogger.error("hellobill.webhook.handler_error", {
599
+ event_id: event.id,
600
+ event_type: event.type,
601
+ message: err.message
602
+ });
603
+ });
604
+ };
605
+ }
606
+
607
+ // src/core/handler.ts
608
+ function decodeSessionToken(token) {
609
+ const parts = token.split(".");
610
+ if (parts.length < 2) {
611
+ throw new SessionTokenError("auth.token_malformed", "Session token is malformed");
612
+ }
613
+ let payload;
614
+ try {
615
+ const raw = Buffer.from(parts[1], "base64url").toString("utf8");
616
+ payload = JSON.parse(raw);
617
+ } catch {
618
+ throw new SessionTokenError("auth.token_malformed", "Session token payload unparseable");
619
+ }
620
+ const sessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : typeof payload.sub === "string" && payload.sub.length > 0 ? payload.sub : "";
621
+ if (sessionId === "") {
622
+ throw new SessionTokenError("auth.token_malformed", "Session token missing session_id (or sub)");
623
+ }
624
+ payload.session_id = sessionId;
625
+ return payload;
626
+ }
627
+ var SessionTokenError = class extends Error {
628
+ constructor(code, message) {
629
+ super(message);
630
+ this.code = code;
631
+ }
632
+ };
633
+ function getHeader(req, name) {
634
+ const lower = name.toLowerCase();
635
+ for (const [k, v] of Object.entries(req.headers)) {
636
+ if (k.toLowerCase() === lower) {
637
+ return Array.isArray(v) ? v[0] : v;
638
+ }
639
+ }
640
+ return void 0;
641
+ }
642
+ function extractBearer(req) {
643
+ const auth = getHeader(req, "authorization");
644
+ if (!auth) return void 0;
645
+ const match = /^Bearer\s+(.+)$/i.exec(auth);
646
+ return match?.[1]?.trim();
647
+ }
648
+ function sendError(res, status, code, message, requestId) {
649
+ if (requestId) res.header("X-HelloBill-Request-Id", requestId);
650
+ const type = status === 401 || status === 403 ? "authentication_error" : status === 429 ? "rate_limit_error" : status >= 500 ? "api_error" : "validation_error";
651
+ res.status(status).json({
652
+ error: {
653
+ type,
654
+ code,
655
+ message,
656
+ request_id: requestId ?? ""
657
+ }
658
+ });
659
+ }
660
+ var PASSTHROUGH_HEADER_MAP = {
661
+ "retry-after": "Retry-After",
662
+ "x-ratelimit-limit": "X-RateLimit-Limit",
663
+ "x-ratelimit-remaining": "X-RateLimit-Remaining",
664
+ "x-ratelimit-reset": "X-RateLimit-Reset"
665
+ };
666
+ function passthroughUpstream(res, status, body, requestId, upstreamHeaders) {
667
+ if (requestId) res.header("X-HelloBill-Request-Id", requestId);
668
+ if (upstreamHeaders) {
669
+ for (const [lower, emit] of Object.entries(PASSTHROUGH_HEADER_MAP)) {
670
+ const v = upstreamHeaders.get(lower);
671
+ if (v !== null && v !== void 0 && v !== "") {
672
+ res.header(emit, v);
673
+ }
674
+ }
675
+ }
676
+ res.status(status).json(body);
677
+ }
678
+ function ensureSessionContext(req, res) {
679
+ const token = extractBearer(req);
680
+ if (!token) {
681
+ sendError(res, 401, "auth.token_malformed", "Missing Authorization bearer token");
682
+ return null;
683
+ }
684
+ try {
685
+ const claims = decodeSessionToken(token);
686
+ return { sessionId: claims.session_id };
687
+ } catch (err) {
688
+ const e = err;
689
+ sendError(res, 401, e.code ?? "auth.token_malformed", e.message ?? "Invalid session token");
690
+ return null;
691
+ }
692
+ }
693
+ function inboundIdempotencyKey(req) {
694
+ const v = getHeader(req, "idempotency-key");
695
+ return v && v.trim().length > 0 ? v.trim() : void 0;
696
+ }
697
+ function createHellobillHandler(opts) {
698
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
699
+ const tokenStorage = opts.tokenStorage ?? new InMemoryTokenStorage();
700
+ const idempotencyKeyFactory = opts.idempotencyKeyFactory ?? defaultIdempotencyKey;
701
+ const logger = opts.logger;
702
+ const upstreamTimeoutMs = opts.upstreamTimeoutMs ?? 3e4;
703
+ const tokenManager = new TokenManager({
704
+ clientId: opts.clientId,
705
+ clientSecret: opts.clientSecret,
706
+ apiBaseUrl: opts.apiBaseUrl,
707
+ fetch: fetchImpl,
708
+ storage: tokenStorage,
709
+ logger
710
+ });
711
+ async function safeCallUpstream(res, upstreamOpts) {
712
+ try {
713
+ const result2 = await callUpstream(
714
+ opts.apiBaseUrl,
715
+ tokenManager,
716
+ { ...upstreamOpts, timeoutMs: upstreamOpts.timeoutMs ?? upstreamTimeoutMs },
717
+ fetchImpl
718
+ );
719
+ if (result2.status >= 200 && result2.status < 300 && result2.body !== null && typeof result2.body !== "object") {
720
+ logger?.warn("hellobill.upstream_invalid_response", {
721
+ status: result2.status,
722
+ path: upstreamOpts.path
723
+ });
724
+ sendError(
725
+ res,
726
+ 502,
727
+ "internal.error",
728
+ "Upstream returned a malformed response",
729
+ result2.requestId
730
+ );
731
+ return null;
732
+ }
733
+ return result2;
734
+ } catch (err) {
735
+ if (err instanceof NetworkError) {
736
+ logger?.error("hellobill.upstream_unreachable", {
737
+ kind: err.kind,
738
+ path: upstreamOpts.path
739
+ });
740
+ if (err.kind === "timeout") {
741
+ sendError(res, 504, "internal.error", "Upstream request timed out");
742
+ } else {
743
+ sendError(res, 502, "internal.error", "Upstream request failed");
744
+ }
745
+ return null;
746
+ }
747
+ throw err;
748
+ }
749
+ }
750
+ const session = async (req, res) => {
751
+ let partnerSessionResult = null;
752
+ if (opts.partnerSession) {
753
+ try {
754
+ partnerSessionResult = await opts.partnerSession(req);
755
+ } catch (err) {
756
+ logger?.warn("hellobill.partner_session_resolver_failed", {
757
+ message: err.message
758
+ });
759
+ sendError(res, 500, "internal.error", "Partner session resolver failed");
760
+ return;
761
+ }
762
+ if (partnerSessionResult === null) {
763
+ sendError(res, 401, "auth.invalid_credentials", "Partner session is not established");
764
+ return;
765
+ }
766
+ }
767
+ let payload;
768
+ try {
769
+ payload = await opts.buildSessionPayload(req, partnerSessionResult);
770
+ } catch (err) {
771
+ logger?.warn("hellobill.build_session_payload_failed", {
772
+ message: err.message
773
+ });
774
+ sendError(res, 400, "validation.invalid_field_value", err.message);
775
+ return;
776
+ }
777
+ const result2 = await safeCallUpstream(res, {
778
+ method: "POST",
779
+ path: "/partner/sessions",
780
+ body: payload,
781
+ idempotencyKey: inboundIdempotencyKey(req) ?? idempotencyKeyFactory(),
782
+ logger
783
+ });
784
+ if (!result2) return;
785
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
786
+ };
787
+ const loa = async (req, res) => {
788
+ const ctx = ensureSessionContext(req, res);
789
+ if (!ctx) return;
790
+ const rawProductIds = req.query.product_ids;
791
+ const productIds = Array.isArray(rawProductIds) ? rawProductIds : typeof rawProductIds === "string" ? rawProductIds.split(",").map((s) => s.trim()).filter(Boolean) : [];
792
+ const result2 = await safeCallUpstream(res, {
793
+ method: "GET",
794
+ path: `/partner/sessions/${encodeURIComponent(ctx.sessionId)}/loa`,
795
+ query: { product_ids: productIds.join(",") || void 0 },
796
+ logger
797
+ });
798
+ if (!result2) return;
799
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
800
+ };
801
+ const customers = async (req, res) => {
802
+ const ctx = ensureSessionContext(req, res);
803
+ if (!ctx) return;
804
+ const result2 = await safeCallUpstream(res, {
805
+ method: "POST",
806
+ path: `/partner/sessions/${encodeURIComponent(ctx.sessionId)}/customers`,
807
+ body: req.body,
808
+ idempotencyKey: inboundIdempotencyKey(req) ?? idempotencyKeyFactory(),
809
+ logger
810
+ });
811
+ if (!result2) return;
812
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
813
+ };
814
+ const bankDetailsSubmit = async (req, res) => {
815
+ const ctx = ensureSessionContext(req, res);
816
+ if (!ctx) return;
817
+ const result2 = await safeCallUpstream(res, {
818
+ method: "POST",
819
+ path: `/partner/sessions/${encodeURIComponent(ctx.sessionId)}/bank-details`,
820
+ body: req.body,
821
+ idempotencyKey: inboundIdempotencyKey(req) ?? idempotencyKeyFactory(),
822
+ logger
823
+ });
824
+ if (!result2) return;
825
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
826
+ };
827
+ const bankDetailsGet = async (req, res) => {
828
+ const ctx = ensureSessionContext(req, res);
829
+ if (!ctx) return;
830
+ const result2 = await safeCallUpstream(res, {
831
+ method: "GET",
832
+ path: `/partner/sessions/${encodeURIComponent(ctx.sessionId)}/bank-details`,
833
+ logger
834
+ });
835
+ if (!result2) return;
836
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
837
+ };
838
+ const status = async (req, res) => {
839
+ const ctx = ensureSessionContext(req, res);
840
+ if (!ctx) return;
841
+ const rawCustomer = req.query.customer_id;
842
+ const customerId = Array.isArray(rawCustomer) ? rawCustomer[0] : rawCustomer;
843
+ if (!customerId) {
844
+ sendError(res, 400, "validation.missing_required_field", "customer_id query parameter required");
845
+ return;
846
+ }
847
+ const result2 = await safeCallUpstream(res, {
848
+ method: "GET",
849
+ path: `/partner/customers/${encodeURIComponent(customerId)}/status`,
850
+ logger
851
+ });
852
+ if (!result2) return;
853
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
854
+ };
855
+ const products = async (req, res) => {
856
+ const ctx = ensureSessionContext(req, res);
857
+ if (!ctx) return;
858
+ const query = {};
859
+ const keys = ["cursor", "limit", "categories", "include_previous", "force_refresh"];
860
+ for (const k of keys) {
861
+ const v = req.query[k];
862
+ if (Array.isArray(v)) query[k] = v[0];
863
+ else if (typeof v === "string") query[k] = v;
864
+ }
865
+ const result2 = await safeCallUpstream(res, {
866
+ method: "GET",
867
+ path: `/partner/sessions/${encodeURIComponent(ctx.sessionId)}/products`,
868
+ query,
869
+ logger
870
+ });
871
+ if (!result2) return;
872
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
873
+ };
874
+ const property = async (req, res) => {
875
+ const ctx = ensureSessionContext(req, res);
876
+ if (!ctx) return;
877
+ const query = {};
878
+ const rawForce = req.query.force_refresh;
879
+ if (typeof rawForce === "string" && rawForce.length > 0) {
880
+ query.force_refresh = rawForce;
881
+ } else if (Array.isArray(rawForce) && typeof rawForce[0] === "string" && rawForce[0].length > 0) {
882
+ query.force_refresh = rawForce[0];
883
+ }
884
+ const result2 = await safeCallUpstream(res, {
885
+ method: "GET",
886
+ path: `/partner/sessions/${encodeURIComponent(ctx.sessionId)}/property`,
887
+ query,
888
+ logger
889
+ });
890
+ if (!result2) return;
891
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
892
+ };
893
+ const sessionsList = async (req, res) => {
894
+ const query = {};
895
+ for (const [k, v] of Object.entries(req.query)) {
896
+ if (typeof v === "string" && v.length > 0) {
897
+ query[k] = v;
898
+ } else if (Array.isArray(v) && typeof v[0] === "string" && v[0].length > 0) {
899
+ query[k] = v[0];
900
+ }
901
+ }
902
+ const result2 = await safeCallUpstream(res, {
903
+ method: "GET",
904
+ path: "/partner/sessions",
905
+ query,
906
+ logger
907
+ });
908
+ if (!result2) return;
909
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
910
+ };
911
+ const sessionsGet = async (req, res) => {
912
+ const id = typeof req.params?.id === "string" ? req.params.id : void 0;
913
+ if (!id || id.length === 0) {
914
+ sendError(res, 400, "validation.missing_required_field", "sessions/:id requires a non-empty id path param");
915
+ return;
916
+ }
917
+ const result2 = await safeCallUpstream(res, {
918
+ method: "GET",
919
+ path: `/partner/sessions/${encodeURIComponent(id)}`,
920
+ logger
921
+ });
922
+ if (!result2) return;
923
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
924
+ };
925
+ const savingsSummary = async (req, res) => {
926
+ const rawCustomer = req.query.customer_id ?? req.params?.id;
927
+ const customerId = Array.isArray(rawCustomer) ? rawCustomer[0] : rawCustomer;
928
+ if (!customerId) {
929
+ sendError(res, 400, "validation.missing_required_field", "customer_id query parameter required");
930
+ return;
931
+ }
932
+ const result2 = await safeCallUpstream(res, {
933
+ method: "GET",
934
+ path: `/partner/customers/${encodeURIComponent(customerId)}/savings-summary`,
935
+ logger
936
+ });
937
+ if (!result2) return;
938
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
939
+ };
940
+ const anyvanSlots = async (req, res) => {
941
+ const rawSession = req.query.session_id ?? req.params?.id;
942
+ const sessionId = Array.isArray(rawSession) ? rawSession[0] : rawSession;
943
+ if (!sessionId) {
944
+ sendError(res, 400, "validation.missing_required_field", "session_id query parameter required");
945
+ return;
946
+ }
947
+ const result2 = await safeCallUpstream(res, {
948
+ method: "GET",
949
+ path: `/partner/sessions/${encodeURIComponent(sessionId)}/anyvan-slots`,
950
+ logger
951
+ });
952
+ if (!result2) return;
953
+ passthroughUpstream(res, result2.status, result2.body, result2.requestId, result2.headers);
954
+ };
955
+ const webhookHandler = opts.webhook ? createWebhookHandler({ ...opts.webhook, logger: opts.webhook.logger ?? opts.logger }) : void 0;
956
+ const result = {
957
+ session,
958
+ loa,
959
+ bankDetails: { submit: bankDetailsSubmit, get: bankDetailsGet },
960
+ customers,
961
+ status,
962
+ products,
963
+ property,
964
+ sessions: { list: sessionsList, get: sessionsGet },
965
+ savingsSummary,
966
+ anyvanSlots
967
+ };
968
+ if (webhookHandler !== void 0) {
969
+ result.webhook = webhookHandler;
970
+ }
971
+ return result;
972
+ }
973
+
974
+ // src/client/index.ts
975
+ var DEFAULT_API_BASE_URL = "https://api.hellobill.app/api/v1";
976
+ var HelloBillPartner = class {
977
+ tokenManager;
978
+ apiBaseUrl;
979
+ fetchImpl;
980
+ logger;
981
+ upstreamTimeoutMs;
982
+ idempotencyKeyFactory;
983
+ sessions;
984
+ customers;
985
+ bankDetails;
986
+ constructor(opts) {
987
+ this.apiBaseUrl = (opts.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
988
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
989
+ this.logger = opts.logger;
990
+ this.upstreamTimeoutMs = opts.upstreamTimeoutMs ?? 3e4;
991
+ this.idempotencyKeyFactory = opts.idempotencyKeyFactory ?? defaultIdempotencyKey;
992
+ this.tokenManager = new TokenManager({
993
+ clientId: opts.clientId,
994
+ clientSecret: opts.clientSecret,
995
+ apiBaseUrl: this.apiBaseUrl,
996
+ fetch: this.fetchImpl,
997
+ storage: opts.tokenStorage ?? new InMemoryTokenStorage(),
998
+ logger: this.logger
999
+ });
1000
+ this.sessions = {
1001
+ create: (payload, callOpts) => this._call({
1002
+ method: "POST",
1003
+ path: "/partner/sessions",
1004
+ body: payload,
1005
+ idempotencyKey: callOpts?.idempotencyKey ?? this.idempotencyKeyFactory()
1006
+ }),
1007
+ list: (query) => this._call({
1008
+ method: "GET",
1009
+ path: "/partner/sessions",
1010
+ query: this._sessionsListQuery(query)
1011
+ }),
1012
+ get: (sessionId) => this._call({
1013
+ method: "GET",
1014
+ path: `/partner/sessions/${encodeURIComponent(sessionId)}`
1015
+ }),
1016
+ products: (sessionId, query) => this._call({
1017
+ method: "GET",
1018
+ path: `/partner/sessions/${encodeURIComponent(sessionId)}/products`,
1019
+ query: this._productsQuery(query)
1020
+ }),
1021
+ property: (sessionId, query) => this._call({
1022
+ method: "GET",
1023
+ path: `/partner/sessions/${encodeURIComponent(sessionId)}/property`,
1024
+ query: this._propertyQuery(query)
1025
+ }),
1026
+ loa: (sessionId, query) => {
1027
+ if (query.product_ids.length === 0) {
1028
+ return Promise.reject(new TypeError("product_ids must contain at least one ID"));
1029
+ }
1030
+ return this._call({
1031
+ method: "GET",
1032
+ path: `/partner/sessions/${encodeURIComponent(sessionId)}/loa`,
1033
+ query: { product_ids: query.product_ids.join(",") }
1034
+ });
1035
+ },
1036
+ customers: (sessionId, payload, callOpts) => this._call({
1037
+ method: "POST",
1038
+ path: `/partner/sessions/${encodeURIComponent(sessionId)}/customers`,
1039
+ body: payload,
1040
+ idempotencyKey: callOpts?.idempotencyKey ?? this.idempotencyKeyFactory()
1041
+ })
1042
+ };
1043
+ this.customers = {
1044
+ status: (customerId) => this._call({
1045
+ method: "GET",
1046
+ path: `/partner/customers/${encodeURIComponent(customerId)}/status`
1047
+ })
1048
+ };
1049
+ this.bankDetails = {
1050
+ submit: (sessionId, payload, callOpts) => this._call({
1051
+ method: "POST",
1052
+ path: `/partner/sessions/${encodeURIComponent(sessionId)}/bank-details`,
1053
+ body: payload,
1054
+ idempotencyKey: callOpts?.idempotencyKey ?? this.idempotencyKeyFactory()
1055
+ }),
1056
+ get: (sessionId) => this._call({
1057
+ method: "GET",
1058
+ path: `/partner/sessions/${encodeURIComponent(sessionId)}/bank-details`
1059
+ })
1060
+ };
1061
+ }
1062
+ /**
1063
+ * Shared transport envelope: acquires a token, builds the request,
1064
+ * delegates to callUpstream (which handles retries and token refresh),
1065
+ * then throws HellobillApiError for any non-2xx response.
1066
+ */
1067
+ async _call(opts) {
1068
+ const result = await callUpstream(
1069
+ this.apiBaseUrl,
1070
+ this.tokenManager,
1071
+ {
1072
+ method: opts.method,
1073
+ path: opts.path,
1074
+ query: opts.query,
1075
+ body: opts.body,
1076
+ idempotencyKey: opts.idempotencyKey,
1077
+ logger: this.logger,
1078
+ timeoutMs: this.upstreamTimeoutMs
1079
+ },
1080
+ this.fetchImpl
1081
+ );
1082
+ if (result.status >= 200 && result.status < 300) {
1083
+ return result.body;
1084
+ }
1085
+ throw new HellobillApiError(
1086
+ result.status,
1087
+ result.body,
1088
+ result.requestId,
1089
+ result.headers
1090
+ );
1091
+ }
1092
+ /** Maps SessionsListQuery to a plain string record for URLSearchParams. */
1093
+ _sessionsListQuery(q) {
1094
+ if (!q) return void 0;
1095
+ const out = {};
1096
+ if (q.email !== void 0) out.email = q.email;
1097
+ if (q.postcode !== void 0) out.postcode = q.postcode;
1098
+ if (q.created_after !== void 0) out.created_after = q.created_after;
1099
+ if (q.created_before !== void 0) out.created_before = q.created_before;
1100
+ if (q.status !== void 0) out.status = q.status;
1101
+ if (q.cursor !== void 0) out.cursor = q.cursor;
1102
+ if (q.limit !== void 0) out.limit = String(q.limit);
1103
+ return out;
1104
+ }
1105
+ /**
1106
+ * Maps ProductsQuery to a wire-safe string record.
1107
+ * - categories: joined as a single CSV value (one URL param, not repeated keys)
1108
+ * - booleans: true → 'true'; false → omitted (no falsy leakage onto the wire)
1109
+ * - numbers: coerced to string
1110
+ */
1111
+ _productsQuery(q) {
1112
+ if (!q) return void 0;
1113
+ const out = {};
1114
+ if (q.categories !== void 0 && q.categories.length > 0) {
1115
+ out.categories = q.categories.join(",");
1116
+ }
1117
+ if (q.cursor !== void 0) out.cursor = q.cursor;
1118
+ if (q.limit !== void 0) out.limit = String(q.limit);
1119
+ if (q.force_refresh === true) out.force_refresh = "true";
1120
+ if (q.include_previous === true) out.include_previous = "true";
1121
+ return out;
1122
+ }
1123
+ /** Maps PropertyQuery to a wire-safe string record. */
1124
+ _propertyQuery(q) {
1125
+ if (!q) return void 0;
1126
+ const out = {};
1127
+ if (q.force_refresh === true) out.force_refresh = "true";
1128
+ return out;
1129
+ }
1130
+ };
1131
+
1132
+ // src/onboardingInfo.ts
1133
+ function buildOnboardingInfo(_productId, category, location, fields) {
1134
+ if (location === "previous") {
1135
+ return void 0;
1136
+ }
1137
+ if (category === "mobile" || category === "tv_licence" || category === "home_insurance" || category === "breakdown_cover") {
1138
+ return void 0;
1139
+ }
1140
+ let result;
1141
+ if (category === "energy") {
1142
+ result = pickFields(fields, ["meter_read_electricity", "meter_read_gas", "preferred_switch_date"]);
1143
+ } else if (category === "broadband") {
1144
+ result = pickFields(fields, ["preferred_install_date", "existing_line_provider", "keep_existing_number"]);
1145
+ } else if (category === "water") {
1146
+ result = pickFields(fields, ["moved_in_date", "meter_read"]);
1147
+ } else if (category === "council_tax") {
1148
+ result = pickFields(fields, ["moved_in_date"]);
1149
+ }
1150
+ return result;
1151
+ }
1152
+ function pickFields(fields, keys) {
1153
+ const picked = {};
1154
+ let hasAny = false;
1155
+ for (const key of keys) {
1156
+ if (Object.prototype.hasOwnProperty.call(fields, key) && fields[key] !== void 0) {
1157
+ picked[key] = fields[key];
1158
+ hasAny = true;
1159
+ }
1160
+ }
1161
+ return hasAny ? picked : void 0;
1162
+ }
1163
+
1164
+ exports.HelloBillPartner = HelloBillPartner;
1165
+ exports.HellobillApiError = HellobillApiError;
1166
+ exports.InMemoryTokenStorage = InMemoryTokenStorage;
1167
+ exports.InMemoryWebhookDedupeStore = InMemoryWebhookDedupeStore;
1168
+ exports.TokenManager = TokenManager;
1169
+ exports.WebhookVerificationError = WebhookVerificationError;
1170
+ exports.buildOnboardingInfo = buildOnboardingInfo;
1171
+ exports.createHellobillHandler = createHellobillHandler;
1172
+ exports.createWebhookHandler = createWebhookHandler;
1173
+ exports.defaultIdempotencyKey = defaultIdempotencyKey;
1174
+ exports.retryFetch = retryFetch;
1175
+ exports.verifyWebhook = verifyWebhook;
1176
+ exports.verifyWebhookSignature = verifyWebhookSignature;
1177
+ //# sourceMappingURL=index.js.map
1178
+ //# sourceMappingURL=index.js.map