@timbro/payments 0.1.0-next.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.
@@ -0,0 +1,670 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import createClient from "openapi-fetch";
3
+ import { verifySignedWebhookBody } from "./webhook-signature.js";
4
+ const TRAILING_SLASH = /\/$/;
5
+ const CONTENT_DIGEST = /^sha-256=:([A-Za-z0-9+/]+={0,2}):$/;
6
+ const DELTA_SECONDS = /^\d+$/;
7
+ const EVENT_ID = /^event_[0-9a-hjkmnp-tv-z]{24,}$/;
8
+ const PAYMENT_ID = /^payment_[0-9a-hjkmnp-tv-z]{24,}$/;
9
+ const FISCAL_DOCUMENT_ID = /^fiscal_document_[0-9a-hjkmnp-tv-z]{24,}$/;
10
+ const MERCHANT_RNC = /^\d{9}$/;
11
+ const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
12
+ export class PaymentsError extends Error {
13
+ status;
14
+ code;
15
+ title;
16
+ requestId;
17
+ problems;
18
+ retryAfterSeconds;
19
+ isRetryable;
20
+ constructor(input) {
21
+ super(input.title, input.cause === undefined ? undefined : { cause: input.cause });
22
+ this.name = "PaymentsError";
23
+ this.status = input.status;
24
+ this.code = input.code;
25
+ this.title = input.title;
26
+ this.requestId = input.requestId;
27
+ this.problems = input.problems ?? [];
28
+ this.retryAfterSeconds = input.retryAfterSeconds;
29
+ this.isRetryable =
30
+ input.code === "transport_failure" ||
31
+ input.retryAfterSeconds !== undefined ||
32
+ RETRYABLE_STATUSES.has(input.status);
33
+ }
34
+ }
35
+ export function createTimbroPayments(options) {
36
+ const secretKey = options.secretKey;
37
+ if (secretKey === undefined || secretKey.trim() === "") {
38
+ throw new TypeError("Timbro Payments secretKey is required.");
39
+ }
40
+ const baseUrl = (options.baseUrl ?? "https://api.timbro.dev").replace(TRAILING_SLASH, "");
41
+ const transport = options.fetch ?? globalThis.fetch;
42
+ const fetcher = async (input, init) => {
43
+ try {
44
+ return await transport(input, init);
45
+ }
46
+ catch (cause) {
47
+ throw transportError(cause);
48
+ }
49
+ };
50
+ const headers = {
51
+ authorization: `Bearer ${secretKey}`,
52
+ };
53
+ const client = createClient({ baseUrl, fetch: fetcher, headers });
54
+ const listPayments = async (params) => {
55
+ const response = await client.GET("/v1/payments", params === undefined
56
+ ? {}
57
+ : { params: { query: params } });
58
+ return decodePaymentList(requireData(response));
59
+ };
60
+ const listWebhookEndpoints = async (params) => {
61
+ const response = await client.GET("/v1/webhook_endpoints", params === undefined
62
+ ? {}
63
+ : { params: { query: params } });
64
+ return decodeWebhookEndpointList(requireData(response));
65
+ };
66
+ const listWebhookDeliveries = async (webhookEndpointId, params) => {
67
+ const response = await client.GET("/v1/webhook_endpoints/{webhookEndpointId}/deliveries", {
68
+ params: {
69
+ path: { webhookEndpointId },
70
+ ...(params === undefined
71
+ ? {}
72
+ : { query: params }),
73
+ },
74
+ });
75
+ return decodeWebhookDeliveryList(requireData(response));
76
+ };
77
+ const listMerchantAccounts = async (params) => {
78
+ const response = await client.GET("/v1/merchant_accounts", params === undefined
79
+ ? {}
80
+ : { params: { query: params } });
81
+ return decodeMerchantAccountList(requireData(response));
82
+ };
83
+ const listRefunds = async (params) => {
84
+ const response = await client.GET("/v1/refunds", params === undefined
85
+ ? {}
86
+ : {
87
+ params: {
88
+ query: params,
89
+ },
90
+ });
91
+ return requireData(response);
92
+ };
93
+ return {
94
+ payments: {
95
+ async create(command) {
96
+ const { idempotencyKey, ...body } = command;
97
+ const response = await client.POST("/v1/payments", {
98
+ body: createPaymentBody(body),
99
+ params: { header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) } },
100
+ });
101
+ return decodePayment(requireData(response));
102
+ },
103
+ async retrieve(paymentId) {
104
+ return decodePayment(requireData(await client.GET("/v1/payments/{paymentId}", { params: { path: { paymentId } } })));
105
+ },
106
+ async cancel(command) {
107
+ const { paymentId, idempotencyKey } = command;
108
+ return decodePayment(requireData(await client.POST("/v1/payments/{paymentId}/cancel", {
109
+ params: {
110
+ path: { paymentId },
111
+ header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) },
112
+ },
113
+ })));
114
+ },
115
+ async attachExternalFiscalDocumentReference(command) {
116
+ const { paymentId, idempotencyKey, ...body } = command;
117
+ return decodePayment(requireData(await client.POST("/v1/payments/{paymentId}/fiscal-document-reference", {
118
+ params: {
119
+ path: { paymentId },
120
+ header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) },
121
+ },
122
+ body: body,
123
+ })));
124
+ },
125
+ async refund(command) {
126
+ const response = await client.POST("/v1/payments/{paymentId}/refunds", {
127
+ params: {
128
+ path: { paymentId: command.paymentId },
129
+ header: { "idempotency-key": requireIdempotencyKey(command.idempotencyKey) },
130
+ },
131
+ });
132
+ return decodeRefund(requireData(response));
133
+ },
134
+ list: listPayments,
135
+ listAll(params) {
136
+ return walkCollection((cursor) => listPayments({ ...params, ...(cursor === undefined ? {} : { cursor }) }));
137
+ },
138
+ },
139
+ refunds: {
140
+ async create(command) {
141
+ const { idempotencyKey, ...body } = command;
142
+ return decodeRefund(requireData(await client.POST("/v1/refunds", {
143
+ body: body,
144
+ params: { header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) } },
145
+ })));
146
+ },
147
+ async attachFiscalCoordination(command) {
148
+ const { refundId, idempotencyKey, ...body } = command;
149
+ return decodeRefund(requireData(await client.POST("/v1/refunds/{refundId}/fiscal-coordination", {
150
+ params: {
151
+ path: { refundId },
152
+ header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) },
153
+ },
154
+ body: body,
155
+ })));
156
+ },
157
+ async retrieve(refundId) {
158
+ return decodeRefund(requireData(await client.GET("/v1/refunds/{refundId}", { params: { path: { refundId } } })));
159
+ },
160
+ list: listRefunds,
161
+ listAll(params) {
162
+ return walkCollection((cursor) => listRefunds({ ...params, ...(cursor === undefined ? {} : { cursor }) }));
163
+ },
164
+ },
165
+ merchantAccounts: {
166
+ list: listMerchantAccounts,
167
+ async retrieveCheckoutCapabilities(merchantAccountId) {
168
+ return requireData(await client.GET("/v1/merchant_accounts/{merchantAccountId}/checkout_capabilities", {
169
+ params: { path: { merchantAccountId } },
170
+ }));
171
+ },
172
+ listAll(params) {
173
+ return walkCollection((cursor) => listMerchantAccounts({ ...params, ...(cursor === undefined ? {} : { cursor }) }));
174
+ },
175
+ async retrieve(merchantAccountId) {
176
+ return decodeMerchantAccount(requireData(await client.GET("/v1/merchant_accounts/{merchantAccountId}", {
177
+ params: { path: { merchantAccountId } },
178
+ })));
179
+ },
180
+ },
181
+ merchantCheckoutPolicy: {
182
+ async retrieve() {
183
+ return requireData(await client.GET("/v1/merchant_checkout_policy"));
184
+ },
185
+ },
186
+ merchantOnboarding: {
187
+ async verifyRnc(input) {
188
+ const response = await client.POST("/v1/merchant_onboarding/rnc_verification", {
189
+ body: { rnc: requireMerchantRnc(input.rnc) },
190
+ });
191
+ return decodeMerchantRncVerification(requireData(response));
192
+ },
193
+ },
194
+ webhookEndpoints: {
195
+ async create(command) {
196
+ const { idempotencyKey, ...body } = command;
197
+ return decodeWebhookEndpoint(requireData(await client.POST("/v1/webhook_endpoints", {
198
+ params: { header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) } },
199
+ body: body,
200
+ })));
201
+ },
202
+ async rotateSecret(command) {
203
+ return decodeWebhookEndpoint(requireData(await client.POST("/v1/webhook_endpoints/{webhookEndpointId}/rotate-secret", {
204
+ params: {
205
+ path: { webhookEndpointId: command.webhookEndpointId },
206
+ header: {
207
+ "idempotency-key": requireIdempotencyKey(command.idempotencyKey),
208
+ },
209
+ },
210
+ })));
211
+ },
212
+ async suspend(webhookEndpointId) {
213
+ return decodeWebhookEndpoint(requireData(await client.POST("/v1/webhook_endpoints/{webhookEndpointId}/suspend", {
214
+ params: { path: { webhookEndpointId } },
215
+ })));
216
+ },
217
+ async reactivate(webhookEndpointId) {
218
+ return decodeWebhookEndpoint(requireData(await client.POST("/v1/webhook_endpoints/{webhookEndpointId}/reactivate", {
219
+ params: { path: { webhookEndpointId } },
220
+ })));
221
+ },
222
+ async delete(webhookEndpointId) {
223
+ requireSuccess(await client.DELETE("/v1/webhook_endpoints/{webhookEndpointId}", {
224
+ params: { path: { webhookEndpointId } },
225
+ }));
226
+ },
227
+ list: listWebhookEndpoints,
228
+ listAll(params) {
229
+ return walkCollection((cursor) => listWebhookEndpoints({ ...params, ...(cursor === undefined ? {} : { cursor }) }));
230
+ },
231
+ async retrieve(webhookEndpointId) {
232
+ return decodeWebhookEndpoint(requireData(await client.GET("/v1/webhook_endpoints/{webhookEndpointId}", {
233
+ params: { path: { webhookEndpointId } },
234
+ })));
235
+ },
236
+ listDeliveries: listWebhookDeliveries,
237
+ listAllDeliveries(webhookEndpointId, params) {
238
+ return walkCollection((cursor) => listWebhookDeliveries(webhookEndpointId, {
239
+ ...params,
240
+ ...(cursor === undefined ? {} : { cursor }),
241
+ }));
242
+ },
243
+ async replay(command) {
244
+ const { webhookEndpointId, idempotencyKey, ...scope } = command;
245
+ return requireData(await client.POST("/v1/webhook_endpoints/{webhookEndpointId}/replays", {
246
+ params: {
247
+ path: { webhookEndpointId },
248
+ header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) },
249
+ },
250
+ body: scope,
251
+ }));
252
+ },
253
+ },
254
+ fiscalDeliveries: {
255
+ async create(command) {
256
+ const { fiscalDocumentId, idempotencyKey, ...body } = command;
257
+ return decodeFiscalDelivery(requireData((await client.POST("/v1/fiscal_documents/{fiscalDocumentId}/deliveries", {
258
+ params: {
259
+ path: { fiscalDocumentId },
260
+ header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) },
261
+ },
262
+ body,
263
+ }))));
264
+ },
265
+ },
266
+ fiscalDocuments: {
267
+ async retrieve(fiscalDocumentId) {
268
+ return decodeFiscalDocument(requireData(await client.GET("/v1/fiscal_documents/{fiscalDocumentId}", {
269
+ params: { path: { fiscalDocumentId } },
270
+ })));
271
+ },
272
+ async correct(command) {
273
+ const { fiscalDocumentId, idempotencyKey, ...body } = command;
274
+ return decodeFiscalDocument(requireData(await client.POST("/v1/fiscal_documents/{fiscalDocumentId}/corrections", {
275
+ params: {
276
+ path: { fiscalDocumentId },
277
+ header: { "idempotency-key": requireIdempotencyKey(idempotencyKey) },
278
+ },
279
+ body: body,
280
+ })));
281
+ },
282
+ retrieveSignedDocument: (fiscalDocumentId) => retrieveArtifact(fetcher, headers, baseUrl, fiscalDocumentId, "signed-document"),
283
+ retrievePrintedRepresentation: (fiscalDocumentId) => retrieveArtifact(fetcher, headers, baseUrl, fiscalDocumentId, "printed-representation"),
284
+ },
285
+ webhooks: { verify: verifyWebhook },
286
+ };
287
+ }
288
+ function createPaymentBody(command) {
289
+ return {
290
+ source: command.source,
291
+ sale: command.sale,
292
+ invoicing: command.invoicing,
293
+ ...(command.returnUrl === undefined ? {} : { returnUrl: command.returnUrl }),
294
+ ...(command.serviceLabel === undefined ? {} : { serviceLabel: command.serviceLabel }),
295
+ ...(command.checkoutDisplay === undefined ? {} : { checkoutDisplay: command.checkoutDisplay }),
296
+ ...(command.merchantAccountId === undefined
297
+ ? {}
298
+ : { merchantAccountId: command.merchantAccountId }),
299
+ ...(command.checkoutPreferences === undefined
300
+ ? {}
301
+ : { checkoutPreferences: command.checkoutPreferences }),
302
+ ...(command.testOptions === undefined ? {} : { testOptions: command.testOptions }),
303
+ };
304
+ }
305
+ function decodePayment(value) {
306
+ return {
307
+ id: value.id,
308
+ object: "payment",
309
+ status: value.status,
310
+ amount: value.amount,
311
+ voluntaryTip: value.voluntaryTip,
312
+ totalAmount: value.totalAmount,
313
+ saleReference: value.saleReference,
314
+ source: value.source,
315
+ ...(value.returnUrl === undefined ? {} : { returnUrl: value.returnUrl }),
316
+ ...(value.serviceLabel === undefined ? {} : { serviceLabel: value.serviceLabel }),
317
+ ...(value.description === undefined ? {} : { description: value.description }),
318
+ ...(value.breakdown === undefined ? {} : { breakdown: value.breakdown }),
319
+ sale: value.sale,
320
+ checkoutDisplay: {
321
+ mode: value.checkoutDisplay.mode,
322
+ wallets: value.checkoutDisplay.wallets,
323
+ },
324
+ ...(value.nextAction === undefined
325
+ ? {}
326
+ : {
327
+ nextAction: {
328
+ type: "redirect",
329
+ url: value.nextAction.url,
330
+ expiresAt: value.nextAction.expiresAt,
331
+ },
332
+ }),
333
+ isLiveMode: value.isLiveMode,
334
+ expiresAt: value.expiresAt,
335
+ createdAt: value.createdAt,
336
+ updatedAt: value.updatedAt,
337
+ fiscalDocument: value.fiscalDocument === null ? null : decodeFiscalDocument(value.fiscalDocument),
338
+ paymentMethodSummary: value.paymentMethodSummary,
339
+ ...(value.capturedPayment === undefined ? {} : { capturedPayment: value.capturedPayment }),
340
+ problem: value.problem,
341
+ split: value.split,
342
+ };
343
+ }
344
+ function decodeRefund(value) {
345
+ if (value.status === "succeeded" && value.completedAt === undefined) {
346
+ throw new Error("Succeeded refund is missing completedAt");
347
+ }
348
+ if (value.status === "declined" &&
349
+ (value.decline === undefined || value.completedAt === undefined)) {
350
+ throw new Error("Declined refund is missing its terminal fields");
351
+ }
352
+ if (value.status === "operator_required" &&
353
+ (value.problem === undefined || value.completedAt === undefined)) {
354
+ throw new Error("Operator-required refund is missing its terminal fields");
355
+ }
356
+ return value;
357
+ }
358
+ function decodePaymentList(value) {
359
+ return {
360
+ object: "list",
361
+ data: value.data.map(decodePayment),
362
+ hasMore: value.hasMore,
363
+ nextCursor: value.nextCursor,
364
+ };
365
+ }
366
+ function decodeFiscalDelivery(value) {
367
+ return "problem" in value
368
+ ? { email: value.email, status: value.status, problem: value.problem }
369
+ : { email: value.email, status: value.status };
370
+ }
371
+ function decodeFiscalDocument(value) {
372
+ const base = {
373
+ id: value.id,
374
+ object: "fiscal_document",
375
+ paymentId: value.paymentId,
376
+ issuanceMode: value.issuanceMode,
377
+ kind: value.kind,
378
+ ...(value.merchantInvoiceNumber === undefined
379
+ ? {}
380
+ : { merchantInvoiceNumber: value.merchantInvoiceNumber }),
381
+ ...(value.merchantOrderNumber === undefined
382
+ ? {}
383
+ : { merchantOrderNumber: value.merchantOrderNumber }),
384
+ revision: value.revision,
385
+ ...(value.buyer === undefined ? {} : { buyer: value.buyer }),
386
+ ...(value.governmentEligibility === undefined
387
+ ? {}
388
+ : { governmentEligibility: value.governmentEligibility }),
389
+ lines: value.lines.map((line) => ({
390
+ sourceLineId: line.sourceLineId,
391
+ description: line.description,
392
+ quantity: line.quantity,
393
+ measurementUnit: line.measurementUnit,
394
+ unitPriceMinorUnits: line.unitPriceMinorUnits,
395
+ ...(line.unitPrice === undefined ? {} : { unitPrice: line.unitPrice }),
396
+ itemKind: line.itemKind,
397
+ taxTreatment: line.taxTreatment,
398
+ isLegalTipApplicable: line.isLegalTipApplicable,
399
+ ...(line.taxIncluded === undefined ? {} : { taxIncluded: line.taxIncluded }),
400
+ ...(line.group === undefined ? {} : { group: line.group }),
401
+ })),
402
+ createdAt: value.createdAt,
403
+ updatedAt: value.updatedAt,
404
+ delivery: value.delivery === null ? null : decodeFiscalDelivery(value.delivery),
405
+ };
406
+ if (value.status === "issued") {
407
+ return {
408
+ ...base,
409
+ status: "issued",
410
+ eNcf: value.eNcf,
411
+ issuedAt: value.issuedAt,
412
+ signedDocument: { sha256: value.signedDocument.sha256 },
413
+ };
414
+ }
415
+ if (value.status === "accepted") {
416
+ return {
417
+ ...base,
418
+ status: "accepted",
419
+ externalReference: value.externalReference,
420
+ printedRepresentation: value.printedRepresentation,
421
+ };
422
+ }
423
+ return { ...base, status: value.status };
424
+ }
425
+ function decodeMerchantAccount(value) {
426
+ return {
427
+ id: value.id,
428
+ object: "merchant_account",
429
+ provider: value.provider,
430
+ mode: value.mode,
431
+ ...(value.venueReference === undefined ? {} : { venueReference: value.venueReference }),
432
+ lifecycle: value.lifecycle,
433
+ ...(value.credentialVersion === undefined
434
+ ? {}
435
+ : { credentialVersion: value.credentialVersion }),
436
+ readiness: {
437
+ status: value.readiness.status,
438
+ blockers: value.readiness.blockers.map(({ code, owner, detail }) => ({
439
+ code,
440
+ owner,
441
+ ...(detail === undefined ? {} : { detail }),
442
+ })),
443
+ checks: value.readiness.checks,
444
+ },
445
+ capabilities: "capabilities" in value ? value.capabilities : [],
446
+ };
447
+ }
448
+ function decodeMerchantAccountList(value) {
449
+ return {
450
+ object: "list",
451
+ data: value.data.map(decodeMerchantAccount),
452
+ hasMore: value.hasMore,
453
+ nextCursor: value.nextCursor,
454
+ };
455
+ }
456
+ function decodeWebhookEndpoint(value) {
457
+ return {
458
+ id: value.id,
459
+ object: "webhook_endpoint",
460
+ url: value.url,
461
+ enabledEvents: value.enabledEvents,
462
+ status: value.status,
463
+ secretCreatedAt: value.secretCreatedAt,
464
+ previousSecretExpiresAt: value.previousSecretExpiresAt,
465
+ createdAt: value.createdAt,
466
+ ...(value.secret === undefined ? {} : { secret: value.secret }),
467
+ };
468
+ }
469
+ function decodeWebhookEndpointList(value) {
470
+ return {
471
+ object: "list",
472
+ data: value.data.map(decodeWebhookEndpoint),
473
+ hasMore: value.hasMore,
474
+ nextCursor: value.nextCursor,
475
+ };
476
+ }
477
+ function decodeWebhookDeliveryList(value) {
478
+ return {
479
+ object: "list",
480
+ data: value.data,
481
+ hasMore: value.hasMore,
482
+ nextCursor: value.nextCursor,
483
+ };
484
+ }
485
+ function requireIdempotencyKey(idempotencyKey) {
486
+ if (idempotencyKey.trim() === "") {
487
+ throw new TypeError("idempotencyKey is required for mutating commands.");
488
+ }
489
+ return idempotencyKey;
490
+ }
491
+ function requireMerchantRnc(rnc) {
492
+ if (!MERCHANT_RNC.test(rnc)) {
493
+ throw new TypeError("rnc must contain exactly nine digits.");
494
+ }
495
+ return rnc;
496
+ }
497
+ function decodeMerchantRncVerification(value) {
498
+ if (!hasExactKeys(value, ["rnc", "legalName"]) ||
499
+ typeof value.rnc !== "string" ||
500
+ !MERCHANT_RNC.test(value.rnc) ||
501
+ typeof value.legalName !== "string" ||
502
+ value.legalName.trim() === "" ||
503
+ value.legalName.length > 256) {
504
+ throw new PaymentsError({
505
+ status: 502,
506
+ code: "unexpected_http_error",
507
+ title: "Timbro Payments returned an invalid merchant RNC verification response.",
508
+ });
509
+ }
510
+ return { rnc: value.rnc, legalName: value.legalName };
511
+ }
512
+ function requireData(response) {
513
+ if (response.data !== undefined) {
514
+ return response.data;
515
+ }
516
+ throw httpError(response.response, response.error);
517
+ }
518
+ function requireSuccess(response) {
519
+ if (!response.response.ok) {
520
+ throw httpError(response.response, response.error);
521
+ }
522
+ }
523
+ function problemPointers(problems) {
524
+ if (!Array.isArray(problems)) {
525
+ return [];
526
+ }
527
+ return problems.flatMap((entry) => {
528
+ if (typeof entry !== "object" || entry === null) {
529
+ return [];
530
+ }
531
+ const { pointer, code } = entry;
532
+ return typeof pointer === "string" && typeof code === "string"
533
+ ? [{ pointer, code: code }]
534
+ : [];
535
+ });
536
+ }
537
+ /** `Retry-After` carries either delta-seconds or an IMF-fixdate. */
538
+ function retryAfterSeconds(header) {
539
+ if (header === null) {
540
+ return undefined;
541
+ }
542
+ const value = header.trim();
543
+ if (DELTA_SECONDS.test(value)) {
544
+ return Number(value);
545
+ }
546
+ const at = Date.parse(value);
547
+ return Number.isNaN(at) ? undefined : Math.max(0, Math.ceil((at - Date.now()) / 1000));
548
+ }
549
+ function httpError(response, body) {
550
+ const problem = typeof body === "object" && body !== null ? body : {};
551
+ const code = typeof problem.code === "string" ? problem.code : undefined;
552
+ const title = typeof problem.title === "string" && problem.title !== ""
553
+ ? problem.title
554
+ : `Timbro Payments returned HTTP ${response.status}.`;
555
+ const requestId = response.headers.get("request-id") ?? undefined;
556
+ const seconds = retryAfterSeconds(response.headers.get("retry-after"));
557
+ return new PaymentsError({
558
+ status: response.status,
559
+ code: code ?? "unexpected_http_error",
560
+ title,
561
+ ...(requestId === undefined ? {} : { requestId }),
562
+ problems: problemPointers(problem.problems),
563
+ ...(seconds === undefined ? {} : { retryAfterSeconds: seconds }),
564
+ });
565
+ }
566
+ /** A page that promises more without minting a fresh cursor would walk the same rows for ever. */
567
+ async function* walkCollection(readPage) {
568
+ const spent = new Set();
569
+ let cursor;
570
+ for (;;) {
571
+ const page = await readPage(cursor);
572
+ yield* page.data;
573
+ if (!page.hasMore) {
574
+ return;
575
+ }
576
+ if (page.nextCursor === null || spent.has(page.nextCursor)) {
577
+ throw new PaymentsError({
578
+ status: 0,
579
+ code: "collection_page_not_advancing",
580
+ title: "Timbro Payments returned a page that promised more rows without advancing.",
581
+ });
582
+ }
583
+ spent.add(page.nextCursor);
584
+ cursor = page.nextCursor;
585
+ }
586
+ }
587
+ function transportError(cause) {
588
+ return new PaymentsError({
589
+ status: 0,
590
+ code: "transport_failure",
591
+ title: "Timbro Payments could not be reached.",
592
+ cause,
593
+ });
594
+ }
595
+ async function retrieveArtifact(fetcher, headers, baseUrl, fiscalDocumentId, artifact) {
596
+ const response = await fetcher(`${baseUrl}/v1/fiscal_documents/${encodeURIComponent(fiscalDocumentId)}/${artifact}`, { headers });
597
+ if (!response.ok) {
598
+ let body;
599
+ try {
600
+ body = await response.json();
601
+ }
602
+ catch {
603
+ body = undefined;
604
+ }
605
+ throw httpError(response, body);
606
+ }
607
+ const bytes = new Uint8Array(await response.arrayBuffer());
608
+ const digest = response.headers.get("content-digest");
609
+ if (digest === null || !verifyContentDigest(bytes, digest)) {
610
+ throw new PaymentsError({
611
+ status: 503,
612
+ code: "artifact_digest_mismatch",
613
+ title: "The fiscal artifact failed Content-Digest verification.",
614
+ });
615
+ }
616
+ return {
617
+ bytes,
618
+ mediaType: response.headers.get("content-type") ?? "application/octet-stream",
619
+ contentDigest: digest,
620
+ };
621
+ }
622
+ function verifyContentDigest(bytes, header) {
623
+ const match = CONTENT_DIGEST.exec(header);
624
+ if (match?.[1] === undefined) {
625
+ return false;
626
+ }
627
+ const expected = Buffer.from(match[1], "base64");
628
+ const actual = createHash("sha256").update(bytes).digest();
629
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
630
+ }
631
+ function verifyWebhook(command) {
632
+ const signed = verifySignedWebhookBody(command);
633
+ if (!isWebhookEvent(signed.payload) || signed.payload.id !== signed.webhookId) {
634
+ throw new TypeError("Invalid Timbro webhook event.");
635
+ }
636
+ return signed.payload;
637
+ }
638
+ function isWebhookEvent(value) {
639
+ if (!hasExactKeys(value, ["id", "type", "apiVersion", "occurredAt", "data"])) {
640
+ return false;
641
+ }
642
+ if (typeof value.id !== "string" ||
643
+ !EVENT_ID.test(value.id) ||
644
+ typeof value.apiVersion !== "string" ||
645
+ typeof value.occurredAt !== "string" ||
646
+ Number.isNaN(Date.parse(value.occurredAt)) ||
647
+ !hasExactKeys(value.data, ["id", "object"]) ||
648
+ typeof value.data.id !== "string") {
649
+ return false;
650
+ }
651
+ if (value.type === "payment.succeeded" ||
652
+ value.type === "payment.canceled" ||
653
+ value.type === "payment.operator_required") {
654
+ return PAYMENT_ID.test(value.data.id) && value.data.object === "payment";
655
+ }
656
+ return ((value.type === "fiscal_document.issued" ||
657
+ value.type === "fiscal_document.rejected" ||
658
+ value.type === "fiscal_document.operator_required") &&
659
+ FISCAL_DOCUMENT_ID.test(value.data.id) &&
660
+ value.data.object === "fiscal_document");
661
+ }
662
+ function hasExactKeys(value, expected) {
663
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
664
+ return false;
665
+ }
666
+ const keys = Object.keys(value).sort();
667
+ const expectedKeys = [...expected].sort();
668
+ return (keys.length === expectedKeys.length && keys.every((key, index) => key === expectedKeys[index]));
669
+ }
670
+ //# sourceMappingURL=payments-client.js.map