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