better-ship 0.20.0 → 0.22.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.
Files changed (90) hide show
  1. package/dist/{application-Dg1jIKPe.js → application-D5tNECiC.js} +2 -2
  2. package/dist/application-D5tNECiC.js.map +1 -0
  3. package/dist/application.d.ts +2 -2
  4. package/dist/application.js +1 -1
  5. package/dist/better-auth/apple-client-secret.js +1 -1
  6. package/dist/better-auth/auth-client.js +1 -1
  7. package/dist/better-auth/rate-limit.js +1 -1
  8. package/dist/better-auth.js +1 -1
  9. package/dist/billing/client.js +1 -1
  10. package/dist/cloudflare/auth-secondary-storage.js +1 -1
  11. package/dist/cloudflare/durable-object-client.js +1 -1
  12. package/dist/cloudflare/durable-object-errors.js +1 -1
  13. package/dist/cloudflare/kv-client.js +1 -1
  14. package/dist/cloudflare/kv-errors.js +1 -1
  15. package/dist/cloudflare/queue.d.ts +1 -1
  16. package/dist/cloudflare/queue.js +2 -2
  17. package/dist/cloudflare/r2-client.js +1 -1
  18. package/dist/cloudflare/r2-errors.js +1 -1
  19. package/dist/cloudflare/scheduled.js +1 -1
  20. package/dist/cloudflare.d.ts +2 -2
  21. package/dist/cloudflare.js +3 -3
  22. package/dist/{core-BT1dSEQI.js → core-BIas38R0.js} +2 -2
  23. package/dist/core-BIas38R0.js.map +1 -0
  24. package/dist/core.js +1 -1
  25. package/dist/http.js +2 -2
  26. package/dist/{index-CYHXOH08.d.ts → index-tNZHSCpJ.d.ts} +6 -2
  27. package/dist/index-tNZHSCpJ.d.ts.map +1 -0
  28. package/dist/{outbox-repository-options-B8TnAE1h.js → outbox-repository-options-D0R8z1yS.js} +2 -2
  29. package/dist/{outbox-repository-options-B8TnAE1h.js.map → outbox-repository-options-D0R8z1yS.js.map} +1 -1
  30. package/dist/{outbox-repository-options-DjBgFhMi.d.ts → outbox-repository-options-_B8l8l6l.d.ts} +2 -2
  31. package/dist/{outbox-repository-options-DjBgFhMi.d.ts.map → outbox-repository-options-_B8l8l6l.d.ts.map} +1 -1
  32. package/dist/postgres.d.ts +2 -2
  33. package/dist/postgres.js +3 -3
  34. package/dist/posthog/metrics.d.ts +1 -1
  35. package/dist/posthog/server.d.ts +1 -1
  36. package/dist/posthog/server.js +1 -1
  37. package/dist/resend.d.ts +14 -16
  38. package/dist/resend.d.ts.map +1 -1
  39. package/dist/resend.js +42 -19
  40. package/dist/resend.js.map +1 -1
  41. package/dist/sentry/client.js +1 -1
  42. package/dist/sentry/server.d.ts +1 -1
  43. package/dist/stripe/client.d.ts +13 -0
  44. package/dist/stripe/client.d.ts.map +1 -0
  45. package/dist/stripe/client.js +20 -0
  46. package/dist/stripe/client.js.map +1 -0
  47. package/dist/stripe/event-map.d.ts +14 -0
  48. package/dist/stripe/event-map.d.ts.map +1 -0
  49. package/dist/stripe/event-map.js +19 -0
  50. package/dist/stripe/event-map.js.map +1 -0
  51. package/dist/stripe/payment-service.d.ts +4 -5
  52. package/dist/stripe/payment-service.d.ts.map +1 -1
  53. package/dist/stripe/payment-service.error.js +2 -2
  54. package/dist/stripe/payment-service.error.js.map +1 -1
  55. package/dist/stripe/payment-service.js +11 -35
  56. package/dist/stripe/payment-service.js.map +1 -1
  57. package/dist/stripe/payment-webhook.js +1 -1
  58. package/dist/stripe/request.d.ts +9 -0
  59. package/dist/stripe/request.d.ts.map +1 -0
  60. package/dist/stripe/request.js +21 -0
  61. package/dist/stripe/request.js.map +1 -0
  62. package/dist/tanstack.js +1 -1
  63. package/dist/testing/outbox-repository.d.ts +1 -1
  64. package/dist/testing/payment-service.d.ts.map +1 -1
  65. package/dist/testing/payment-service.js +12 -12
  66. package/dist/testing/payment-service.js.map +1 -1
  67. package/dist/testing/stripe.d.ts +21 -0
  68. package/dist/testing/stripe.d.ts.map +1 -0
  69. package/dist/testing/stripe.js +33 -0
  70. package/dist/testing/stripe.js.map +1 -0
  71. package/dist/{webhook-verification-qSFXv5H3.js → webhook-verification-CEUARY9X.js} +2 -2
  72. package/dist/{webhook-verification-qSFXv5H3.js.map → webhook-verification-CEUARY9X.js.map} +1 -1
  73. package/package.json +5 -1
  74. package/src/application/email-service.ts +3 -0
  75. package/src/application/index.ts +1 -0
  76. package/src/core/email-service.error.ts +1 -1
  77. package/src/infrastructure/resend/email-event.ts +3 -0
  78. package/src/infrastructure/resend/index.ts +2 -2
  79. package/src/infrastructure/resend/resend-email-service.ts +39 -17
  80. package/src/infrastructure/resend/resend-webhook.ts +15 -16
  81. package/src/infrastructure/stripe/client.ts +16 -0
  82. package/src/infrastructure/stripe/event-map.ts +25 -0
  83. package/src/infrastructure/stripe/payment-service.error.ts +2 -1
  84. package/src/infrastructure/stripe/payment-service.ts +15 -39
  85. package/src/infrastructure/stripe/request.ts +17 -0
  86. package/src/infrastructure/testing/payment-service-contract.ts +11 -14
  87. package/src/infrastructure/testing/stripe-fixture.ts +42 -0
  88. package/dist/application-Dg1jIKPe.js.map +0 -1
  89. package/dist/core-BT1dSEQI.js.map +0 -1
  90. package/dist/index-CYHXOH08.d.ts.map +0 -1
@@ -0,0 +1,9 @@
1
+ //#region src/infrastructure/stripe/request.d.ts
2
+ /**
3
+ * Invoke one SDK operation; the SDK owns retries configured on its client or request.
4
+ * Convert every Stripe error to PaymentServiceError without logging; preserve the original error in cause.
5
+ * Non-Stripe failures propagate unchanged.
6
+ */
7
+ export declare function stripeRequest<T>(operation: string, work: () => Promise<T>): Promise<T>;
8
+ //#endregion
9
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","names":[],"sources":["../../src/infrastructure/stripe/request.ts"],"mappings":";;;;;;wBASsB,cAAc,GAAG,mBAAmB,YAAY,QAAQ,KAAK,QAAQ"}
@@ -0,0 +1,21 @@
1
+ import { PaymentServiceError } from "./payment-service.error.js";
2
+ import Stripe from "stripe";
3
+
4
+ //#region src/infrastructure/stripe/request.ts
5
+ /**
6
+ * Invoke one SDK operation; the SDK owns retries configured on its client or request.
7
+ * Convert every Stripe error to PaymentServiceError without logging; preserve the original error in cause.
8
+ * Non-Stripe failures propagate unchanged.
9
+ */
10
+ async function stripeRequest(operation, work) {
11
+ try {
12
+ return await work();
13
+ } catch (cause) {
14
+ if (!(cause instanceof Stripe.errors.StripeError)) throw cause;
15
+ throw new PaymentServiceError(operation, cause);
16
+ }
17
+ }
18
+
19
+ //#endregion
20
+ export { stripeRequest };
21
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","names":[],"sources":["../../src/infrastructure/stripe/request.ts"],"sourcesContent":["import Stripe from 'stripe'\n\nimport { PaymentServiceError } from './payment-service.error.ts'\n\n/**\n * Invoke one SDK operation; the SDK owns retries configured on its client or request.\n * Convert every Stripe error to PaymentServiceError without logging; preserve the original error in cause.\n * Non-Stripe failures propagate unchanged.\n */\nexport async function stripeRequest<T>(operation: string, work: () => Promise<T>): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n if (!(cause instanceof Stripe.errors.StripeError)) throw cause\n throw new PaymentServiceError(operation, cause)\n }\n}\n"],"mappings":";;;;;;;;;AASA,eAAsB,cAAiB,WAAmB,MAAoC;CAC5F,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,OAAO,OAAO,cAAc,MAAM;EACzD,MAAM,IAAI,oBAAoB,WAAW,KAAK;CAChD;AACF"}
package/dist/tanstack.js CHANGED
@@ -1,4 +1,4 @@
1
- import { H as StaleSessionError, R as ValidationError, W as AuthenticationError, nt as AppError, r as createLogger } from "./core-BT1dSEQI.js";
1
+ import { H as StaleSessionError, R as ValidationError, W as AuthenticationError, nt as AppError, r as createLogger } from "./core-BIas38R0.js";
2
2
  import { z } from "zod";
3
3
  import { getReasonPhrase } from "http-status-codes";
4
4
  import { isNotFound, isRedirect, redirect } from "@tanstack/react-router";
@@ -1,5 +1,5 @@
1
1
  import { W as DomainEvent } from "../index-DUFS0SJ_.js";
2
- import { A as IOutboxRepository } from "../index-CYHXOH08.js";
2
+ import { A as IOutboxRepository } from "../index-tNZHSCpJ.js";
3
3
  //#region src/infrastructure/testing/outbox-repository-contract.d.ts
4
4
  /** Stored fields observed by the adapter contract, independent of a database library. */
5
5
  export type OutboxContractRow = {
@@ -1 +1 @@
1
- {"version":3,"file":"payment-service.d.ts","names":[],"sources":["../../src/infrastructure/testing/payment-service-contract.ts"],"mappings":";;;;;;;wBAUgB,uBACd,OAAO,cAAc,WAAW,wBAChC,SAAS,QAAQ,WAAW"}
1
+ {"version":3,"file":"payment-service.d.ts","names":[],"sources":["../../src/infrastructure/testing/payment-service-contract.ts"],"mappings":";;;;;;;wBAYgB,uBACd,OAAO,cAAc,WAAW,wBAChC,SAAS,QAAQ,WAAW"}
@@ -1,5 +1,7 @@
1
+ import { PaymentServiceError } from "../stripe/payment-service.error.js";
2
+ import { stripeFixture } from "./stripe.js";
1
3
  import Stripe from "stripe";
2
- import { deepStrictEqual, equal, rejects } from "node:assert/strict";
4
+ import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
3
5
 
4
6
  //#region src/infrastructure/testing/payment-service-contract.ts
5
7
  /**
@@ -39,7 +41,7 @@ function paymentServiceContract(test, create) {
39
41
  status: "requires_capture"
40
42
  });
41
43
  });
42
- test("card failures retain the native error contract", async () => {
44
+ test("card failures retain the native error in cause", async () => {
43
45
  const fixture = paymentFixture(create, Response.json({ error: {
44
46
  type: "card_error",
45
47
  code: "authentication_required",
@@ -50,23 +52,21 @@ function paymentServiceContract(test, create) {
50
52
  currency: "usd",
51
53
  customer: "cus_contract",
52
54
  payment_method: "pm_contract"
53
- }, { idempotencyKey: "charge:contract" }), Stripe.errors.StripeCardError);
55
+ }, { idempotencyKey: "charge:contract" }), (error) => {
56
+ ok(error instanceof PaymentServiceError);
57
+ ok(error.cause instanceof Stripe.errors.StripeCardError);
58
+ equal(error.cause.code, "authentication_required");
59
+ return true;
60
+ });
54
61
  });
55
62
  }
56
63
  function paymentFixture(create, response = Response.json({
57
64
  id: "pi_contract",
58
65
  status: "requires_capture"
59
66
  })) {
60
- const requests = [];
67
+ const { stripe, requests } = stripeFixture(() => response.clone());
61
68
  return {
62
- service: create(new Stripe("sk_test_contract", { httpClient: Stripe.createFetchHttpClient(async (input, init) => {
63
- const request = new Request(input, init);
64
- requests.push({
65
- headers: request.headers,
66
- body: new URLSearchParams(new TextDecoder().decode(await request.arrayBuffer()))
67
- });
68
- return response.clone();
69
- }) })),
69
+ service: create(stripe),
70
70
  requests
71
71
  };
72
72
  }
@@ -1 +1 @@
1
- {"version":3,"file":"payment-service.js","names":[],"sources":["../../src/infrastructure/testing/payment-service-contract.ts"],"sourcesContent":["import { deepStrictEqual, equal, rejects } from 'node:assert/strict'\n\nimport Stripe from 'stripe'\n\nimport type { IPaymentService } from '../stripe/payment-service.ts'\n\n/**\n * Test a Stripe payment adapter through native fetch injection without live payments.\n * Each case constructs a fresh SDK instance and adapter; no Vitest dependency is required.\n */\nexport function paymentServiceContract(\n test: (name: string, run: () => Promise<void>) => void,\n create: (stripe: Stripe) => IPaymentService,\n): void {\n test('payment checkout selects payment mode and retains the supplied key', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.createCheckout(\n { customer: 'cus_contract' },\n { idempotencyKey: 'checkout:contract' },\n )\n equal(fixture.requests[0]?.body.get('mode'), 'payment')\n equal(fixture.requests[0]?.headers.get('idempotency-key'), 'checkout:contract')\n })\n\n test('payment method setup selects setup mode', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.setupPaymentMethod(\n { customer: 'cus_contract' },\n { idempotencyKey: 'setup:contract' },\n )\n equal(fixture.requests[0]?.body.get('mode'), 'setup')\n })\n\n test('saved method payments confirm off-session', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.chargeSavedMethod(\n { amount: 1000, currency: 'usd', customer: 'cus_contract', payment_method: 'pm_contract' },\n { idempotencyKey: 'charge:contract' },\n )\n equal(fixture.requests[0]?.body.get('confirm'), 'true')\n equal(fixture.requests[0]?.body.get('off_session'), 'true')\n })\n\n test('payment reads preserve native statuses and response fields', async () => {\n const fixture = paymentFixture(create)\n const payment = await fixture.service.getPayment('pi_contract')\n deepStrictEqual(\n { id: payment.id, status: payment.status },\n { id: 'pi_contract', status: 'requires_capture' },\n )\n })\n\n test('card failures retain the native error contract', async () => {\n const response = Response.json(\n {\n error: {\n type: 'card_error',\n code: 'authentication_required',\n message: 'Authentication required',\n },\n },\n { status: 402 },\n )\n const fixture = paymentFixture(create, response)\n await rejects(\n fixture.service.chargeSavedMethod(\n { amount: 1000, currency: 'usd', customer: 'cus_contract', payment_method: 'pm_contract' },\n { idempotencyKey: 'charge:contract' },\n ),\n Stripe.errors.StripeCardError,\n )\n })\n}\n\nfunction paymentFixture(\n create: (stripe: Stripe) => IPaymentService,\n response = Response.json({ id: 'pi_contract', status: 'requires_capture' }),\n) {\n const requests: { headers: Headers; body: URLSearchParams }[] = []\n const stripe = new Stripe('sk_test_contract', {\n httpClient: Stripe.createFetchHttpClient(async (input, init) => {\n const request = new Request(input, init)\n requests.push({\n headers: request.headers,\n body: new URLSearchParams(new TextDecoder().decode(await request.arrayBuffer())),\n })\n return response.clone()\n }),\n })\n return { service: create(stripe), requests }\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,uBACd,MACA,QACM;CACN,KAAK,sEAAsE,YAAY;EACrF,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,eACpB,EAAE,UAAU,eAAe,GAC3B,EAAE,gBAAgB,oBAAoB,CACxC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,MAAM,GAAG,SAAS;EACtD,MAAM,QAAQ,SAAS,EAAE,EAAE,QAAQ,IAAI,iBAAiB,GAAG,mBAAmB;CAChF,CAAC;CAED,KAAK,2CAA2C,YAAY;EAC1D,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,mBACpB,EAAE,UAAU,eAAe,GAC3B,EAAE,gBAAgB,iBAAiB,CACrC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,MAAM,GAAG,OAAO;CACtD,CAAC;CAED,KAAK,6CAA6C,YAAY;EAC5D,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,kBACpB;GAAE,QAAQ;GAAM,UAAU;GAAO,UAAU;GAAgB,gBAAgB;EAAc,GACzF,EAAE,gBAAgB,kBAAkB,CACtC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,SAAS,GAAG,MAAM;EACtD,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,aAAa,GAAG,MAAM;CAC5D,CAAC;CAED,KAAK,8DAA8D,YAAY;EAE7E,MAAM,UAAU,MADA,eAAe,MACH,CAAC,CAAC,QAAQ,WAAW,aAAa;EAC9D,gBACE;GAAE,IAAI,QAAQ;GAAI,QAAQ,QAAQ;EAAO,GACzC;GAAE,IAAI;GAAe,QAAQ;EAAmB,CAClD;CACF,CAAC;CAED,KAAK,kDAAkD,YAAY;EAWjE,MAAM,UAAU,eAAe,QAVd,SAAS,KACxB,EACE,OAAO;GACL,MAAM;GACN,MAAM;GACN,SAAS;EACX,EACF,GACA,EAAE,QAAQ,IAAI,CAE8B,CAAC;EAC/C,MAAM,QACJ,QAAQ,QAAQ,kBACd;GAAE,QAAQ;GAAM,UAAU;GAAO,UAAU;GAAgB,gBAAgB;EAAc,GACzF,EAAE,gBAAgB,kBAAkB,CACtC,GACA,OAAO,OAAO,eAChB;CACF,CAAC;AACH;AAEA,SAAS,eACP,QACA,WAAW,SAAS,KAAK;CAAE,IAAI;CAAe,QAAQ;AAAmB,CAAC,GAC1E;CACA,MAAM,WAA0D,CAAC;CAWjE,OAAO;EAAE,SAAS,OAAO,IAVN,OAAO,oBAAoB,EAC5C,YAAY,OAAO,sBAAsB,OAAO,OAAO,SAAS;GAC9D,MAAM,UAAU,IAAI,QAAQ,OAAO,IAAI;GACvC,SAAS,KAAK;IACZ,SAAS,QAAQ;IACjB,MAAM,IAAI,gBAAgB,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,QAAQ,YAAY,CAAC,CAAC;GACjF,CAAC;GACD,OAAO,SAAS,MAAM;EACxB,CAAC,EACH,CAC8B,CAAC;EAAG;CAAS;AAC7C"}
1
+ {"version":3,"file":"payment-service.js","names":[],"sources":["../../src/infrastructure/testing/payment-service-contract.ts"],"sourcesContent":["import { deepStrictEqual, equal, ok, rejects } from 'node:assert/strict'\n\nimport Stripe from 'stripe'\n\nimport { PaymentServiceError } from '../stripe/payment-service.error.ts'\nimport type { IPaymentService } from '../stripe/payment-service.ts'\nimport { stripeFixture } from './stripe-fixture.ts'\n\n/**\n * Test a Stripe payment adapter through native fetch injection without live payments.\n * Each case constructs a fresh SDK instance and adapter; no Vitest dependency is required.\n */\nexport function paymentServiceContract(\n test: (name: string, run: () => Promise<void>) => void,\n create: (stripe: Stripe) => IPaymentService,\n): void {\n test('payment checkout selects payment mode and retains the supplied key', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.createCheckout(\n { customer: 'cus_contract' },\n { idempotencyKey: 'checkout:contract' },\n )\n equal(fixture.requests[0]?.body.get('mode'), 'payment')\n equal(fixture.requests[0]?.headers.get('idempotency-key'), 'checkout:contract')\n })\n\n test('payment method setup selects setup mode', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.setupPaymentMethod(\n { customer: 'cus_contract' },\n { idempotencyKey: 'setup:contract' },\n )\n equal(fixture.requests[0]?.body.get('mode'), 'setup')\n })\n\n test('saved method payments confirm off-session', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.chargeSavedMethod(\n { amount: 1000, currency: 'usd', customer: 'cus_contract', payment_method: 'pm_contract' },\n { idempotencyKey: 'charge:contract' },\n )\n equal(fixture.requests[0]?.body.get('confirm'), 'true')\n equal(fixture.requests[0]?.body.get('off_session'), 'true')\n })\n\n test('payment reads preserve native statuses and response fields', async () => {\n const fixture = paymentFixture(create)\n const payment = await fixture.service.getPayment('pi_contract')\n deepStrictEqual(\n { id: payment.id, status: payment.status },\n { id: 'pi_contract', status: 'requires_capture' },\n )\n })\n\n test('card failures retain the native error in cause', async () => {\n const response = Response.json(\n {\n error: {\n type: 'card_error',\n code: 'authentication_required',\n message: 'Authentication required',\n },\n },\n { status: 402 },\n )\n const fixture = paymentFixture(create, response)\n await rejects(\n fixture.service.chargeSavedMethod(\n { amount: 1000, currency: 'usd', customer: 'cus_contract', payment_method: 'pm_contract' },\n { idempotencyKey: 'charge:contract' },\n ),\n (error) => {\n ok(error instanceof PaymentServiceError)\n ok(error.cause instanceof Stripe.errors.StripeCardError)\n equal(error.cause.code, 'authentication_required')\n return true\n },\n )\n })\n}\n\nfunction paymentFixture(\n create: (stripe: Stripe) => IPaymentService,\n response = Response.json({ id: 'pi_contract', status: 'requires_capture' }),\n) {\n const { stripe, requests } = stripeFixture(() => response.clone())\n return { service: create(stripe), requests }\n}\n"],"mappings":";;;;;;;;;;AAYA,SAAgB,uBACd,MACA,QACM;CACN,KAAK,sEAAsE,YAAY;EACrF,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,eACpB,EAAE,UAAU,eAAe,GAC3B,EAAE,gBAAgB,oBAAoB,CACxC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,MAAM,GAAG,SAAS;EACtD,MAAM,QAAQ,SAAS,EAAE,EAAE,QAAQ,IAAI,iBAAiB,GAAG,mBAAmB;CAChF,CAAC;CAED,KAAK,2CAA2C,YAAY;EAC1D,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,mBACpB,EAAE,UAAU,eAAe,GAC3B,EAAE,gBAAgB,iBAAiB,CACrC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,MAAM,GAAG,OAAO;CACtD,CAAC;CAED,KAAK,6CAA6C,YAAY;EAC5D,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,kBACpB;GAAE,QAAQ;GAAM,UAAU;GAAO,UAAU;GAAgB,gBAAgB;EAAc,GACzF,EAAE,gBAAgB,kBAAkB,CACtC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,SAAS,GAAG,MAAM;EACtD,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,aAAa,GAAG,MAAM;CAC5D,CAAC;CAED,KAAK,8DAA8D,YAAY;EAE7E,MAAM,UAAU,MADA,eAAe,MACH,CAAC,CAAC,QAAQ,WAAW,aAAa;EAC9D,gBACE;GAAE,IAAI,QAAQ;GAAI,QAAQ,QAAQ;EAAO,GACzC;GAAE,IAAI;GAAe,QAAQ;EAAmB,CAClD;CACF,CAAC;CAED,KAAK,kDAAkD,YAAY;EAWjE,MAAM,UAAU,eAAe,QAVd,SAAS,KACxB,EACE,OAAO;GACL,MAAM;GACN,MAAM;GACN,SAAS;EACX,EACF,GACA,EAAE,QAAQ,IAAI,CAE8B,CAAC;EAC/C,MAAM,QACJ,QAAQ,QAAQ,kBACd;GAAE,QAAQ;GAAM,UAAU;GAAO,UAAU;GAAgB,gBAAgB;EAAc,GACzF,EAAE,gBAAgB,kBAAkB,CACtC,IACC,UAAU;GACT,GAAG,iBAAiB,mBAAmB;GACvC,GAAG,MAAM,iBAAiB,OAAO,OAAO,eAAe;GACvD,MAAM,MAAM,MAAM,MAAM,yBAAyB;GACjD,OAAO;EACT,CACF;CACF,CAAC;AACH;AAEA,SAAS,eACP,QACA,WAAW,SAAS,KAAK;CAAE,IAAI;CAAe,QAAQ;AAAmB,CAAC,GAC1E;CACA,MAAM,EAAE,QAAQ,aAAa,oBAAoB,SAAS,MAAM,CAAC;CACjE,OAAO;EAAE,SAAS,OAAO,MAAM;EAAG;CAAS;AAC7C"}
@@ -0,0 +1,21 @@
1
+ import Stripe from "stripe";
2
+ //#region src/infrastructure/testing/stripe-fixture.d.ts
3
+ /** One SDK HTTP attempt; query and body expose Stripe's URL-encoded parameters. */
4
+ export type StripeCall = {
5
+ readonly method: string;
6
+ readonly path: string;
7
+ readonly query: URLSearchParams;
8
+ readonly body: URLSearchParams;
9
+ readonly headers: Headers;
10
+ };
11
+ type StripeFixture = {
12
+ stripe: Stripe;
13
+ requests: readonly StripeCall[];
14
+ };
15
+ /**
16
+ * Construct the real SDK with a fake fetch transport and an ordered record of HTTP attempts.
17
+ * The responder receives an unread request. The shared client factory supplies retry defaults.
18
+ */
19
+ export declare function stripeFixture(respond: (request: Request) => Response | Promise<Response>): StripeFixture;
20
+ //#endregion
21
+ //# sourceMappingURL=stripe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe.d.ts","names":[],"sources":["../../src/infrastructure/testing/stripe-fixture.ts"],"mappings":";;;YAKY;WACD;WACA;WACA,OAAO;WACP,MAAM;WACN,SAAS;;KAGf;EAAkB,QAAQ;EAAQ,mBAAmB;;;;;;wBAM1C,cACd,UAAU,SAAS,YAAY,WAAW,QAAQ,YACjD"}
@@ -0,0 +1,33 @@
1
+ import { createStripeClient } from "../stripe/client.js";
2
+ import Stripe from "stripe";
3
+
4
+ //#region src/infrastructure/testing/stripe-fixture.ts
5
+ /**
6
+ * Construct the real SDK with a fake fetch transport and an ordered record of HTTP attempts.
7
+ * The responder receives an unread request. The shared client factory supplies retry defaults.
8
+ */
9
+ function stripeFixture(respond) {
10
+ const requests = [];
11
+ return {
12
+ stripe: createStripeClient("sk_test_fixture", { httpClient: Stripe.createFetchHttpClient(async (input, init) => {
13
+ const request = new Request(input, init);
14
+ const url = new URL(request.url);
15
+ const call = {
16
+ method: request.method,
17
+ path: url.pathname,
18
+ query: url.searchParams,
19
+ body: new URLSearchParams(),
20
+ headers: new Headers(request.headers)
21
+ };
22
+ requests.push(call);
23
+ const body = new TextDecoder().decode(await request.clone().arrayBuffer());
24
+ for (const [key, value] of new URLSearchParams(body)) call.body.append(key, value);
25
+ return respond(request);
26
+ }) }),
27
+ requests
28
+ };
29
+ }
30
+
31
+ //#endregion
32
+ export { stripeFixture };
33
+ //# sourceMappingURL=stripe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe.js","names":[],"sources":["../../src/infrastructure/testing/stripe-fixture.ts"],"sourcesContent":["import Stripe from 'stripe'\n\nimport { createStripeClient } from '../stripe/client.ts'\n\n/** One SDK HTTP attempt; query and body expose Stripe's URL-encoded parameters. */\nexport type StripeCall = {\n readonly method: string\n readonly path: string\n readonly query: URLSearchParams\n readonly body: URLSearchParams\n readonly headers: Headers\n}\n\ntype StripeFixture = { stripe: Stripe; requests: readonly StripeCall[] }\n\n/**\n * Construct the real SDK with a fake fetch transport and an ordered record of HTTP attempts.\n * The responder receives an unread request. The shared client factory supplies retry defaults.\n */\nexport function stripeFixture(\n respond: (request: Request) => Response | Promise<Response>,\n): StripeFixture {\n const requests: StripeCall[] = []\n const stripe = createStripeClient('sk_test_fixture', {\n httpClient: Stripe.createFetchHttpClient(async (input, init) => {\n const request = new Request(input, init)\n const url = new URL(request.url)\n const call: StripeCall = {\n method: request.method,\n path: url.pathname,\n query: url.searchParams,\n body: new URLSearchParams(),\n headers: new Headers(request.headers),\n }\n requests.push(call)\n const body = new TextDecoder().decode(await request.clone().arrayBuffer())\n for (const [key, value] of new URLSearchParams(body)) call.body.append(key, value)\n return respond(request)\n }),\n })\n return { stripe, requests }\n}\n"],"mappings":";;;;;;;;AAmBA,SAAgB,cACd,SACe;CACf,MAAM,WAAyB,CAAC;CAkBhC,OAAO;EAAE,QAjBM,mBAAmB,mBAAmB,EACnD,YAAY,OAAO,sBAAsB,OAAO,OAAO,SAAS;GAC9D,MAAM,UAAU,IAAI,QAAQ,OAAO,IAAI;GACvC,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;GAC/B,MAAM,OAAmB;IACvB,QAAQ,QAAQ;IAChB,MAAM,IAAI;IACV,OAAO,IAAI;IACX,MAAM,IAAI,gBAAgB;IAC1B,SAAS,IAAI,QAAQ,QAAQ,OAAO;GACtC;GACA,SAAS,KAAK,IAAI;GAClB,MAAM,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,QAAQ,MAAM,CAAC,CAAC,YAAY,CAAC;GACzE,KAAK,MAAM,CAAC,KAAK,UAAU,IAAI,gBAAgB,IAAI,GAAG,KAAK,KAAK,OAAO,KAAK,KAAK;GACjF,OAAO,QAAQ,OAAO;EACxB,CAAC,EACH,CACc;EAAG;CAAS;AAC5B"}
@@ -1,4 +1,4 @@
1
- import { nt as AppError } from "./core-BT1dSEQI.js";
1
+ import { nt as AppError } from "./core-BIas38R0.js";
2
2
 
3
3
  //#region src/infrastructure/http/webhook-verification.ts
4
4
  /** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */
@@ -15,4 +15,4 @@ var WebhookVerificationError = class extends AppError {
15
15
 
16
16
  //#endregion
17
17
  export { WebhookVerificationError as t };
18
- //# sourceMappingURL=webhook-verification-qSFXv5H3.js.map
18
+ //# sourceMappingURL=webhook-verification-CEUARY9X.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-verification-qSFXv5H3.js","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"sourcesContent":["import { AppError } from '@/core'\n\n/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */\nexport class WebhookVerificationError extends AppError {\n readonly _tag = 'WebhookVerificationError'\n readonly classification = 'terminal'\n\n /** Keep secrets and payload details out of the public message. */\n constructor(\n readonly reason: 'missing_headers' | 'invalid_signature' | 'invalid_payload',\n cause?: unknown,\n ) {\n super(`Webhook rejected: ${reason}`, { cause })\n }\n}\n\n/** The provider owns signature verification, timestamp tolerance, and payload parsing. */\nexport interface WebhookVerifier<Verified> {\n /**\n * Verify the exact body before parsing it. Return null for an ignored webhook.\n * @throws WebhookVerificationError when the request cannot be trusted or parsed.\n */\n verify(body: string, headers: Headers): Promise<Verified | null>\n}\n"],"mappings":";;;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAM1C;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;;CAG1B,YACE,AAAS,QACT,OACA;EACA,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAHrC;CAIX;AACF"}
1
+ {"version":3,"file":"webhook-verification-CEUARY9X.js","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"sourcesContent":["import { AppError } from '@/core'\n\n/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */\nexport class WebhookVerificationError extends AppError {\n readonly _tag = 'WebhookVerificationError'\n readonly classification = 'terminal'\n\n /** Keep secrets and payload details out of the public message. */\n constructor(\n readonly reason: 'missing_headers' | 'invalid_signature' | 'invalid_payload',\n cause?: unknown,\n ) {\n super(`Webhook rejected: ${reason}`, { cause })\n }\n}\n\n/** The provider owns signature verification, timestamp tolerance, and payload parsing. */\nexport interface WebhookVerifier<Verified> {\n /**\n * Verify the exact body before parsing it. Return null for an ignored webhook.\n * @throws WebhookVerificationError when the request cannot be trusted or parsed.\n */\n verify(body: string, headers: Headers): Promise<Verified | null>\n}\n"],"mappings":";;;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAM1C;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;;CAG1B,YACE,AAAS,QACT,OACA;EACA,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAHrC;CAIX;AACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "better-ship",
4
- "version": "0.20.0",
4
+ "version": "0.22.0",
5
5
  "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -44,13 +44,17 @@
44
44
  "./sentry/client": "./dist/sentry/client.js",
45
45
  "./sentry/server": "./dist/sentry/server.js",
46
46
  "./sentry/vite": "./dist/sentry/vite.js",
47
+ "./stripe/client": "./dist/stripe/client.js",
48
+ "./stripe/event-map": "./dist/stripe/event-map.js",
47
49
  "./stripe/payment-service": "./dist/stripe/payment-service.js",
48
50
  "./stripe/payment-service.error": "./dist/stripe/payment-service.error.js",
49
51
  "./stripe/payment-webhook": "./dist/stripe/payment-webhook.js",
52
+ "./stripe/request": "./dist/stripe/request.js",
50
53
  "./tanstack": "./dist/tanstack.js",
51
54
  "./tanstack/server": "./dist/tanstack/server.js",
52
55
  "./testing/outbox-repository": "./dist/testing/outbox-repository.js",
53
56
  "./testing/payment-service": "./dist/testing/payment-service.js",
57
+ "./testing/stripe": "./dist/testing/stripe.js",
54
58
  "./ui": "./dist/ui.js",
55
59
  "./package.json": "./package.json"
56
60
  },
@@ -32,6 +32,9 @@ export type OutboundEmail = {
32
32
  readonly attachments?: readonly EmailAttachment[]
33
33
  }
34
34
 
35
+ /** Received email headers, without the body or attachments. */
36
+ export type ReceivedEmail = { readonly headers: Readonly<Record<string, string>> }
37
+
35
38
  /** The provider accepted the email and assigned its id. */
36
39
  export type EmailAccepted = { readonly providerId: string }
37
40
 
@@ -19,6 +19,7 @@ export {
19
19
  type EmailAttachment,
20
20
  type MimeType,
21
21
  type EmailAccepted,
22
+ type ReceivedEmail,
22
23
  } from './email-service.ts'
23
24
  export {
24
25
  recordEmailEvent,
@@ -40,7 +40,7 @@ export class EmailServiceError extends AppError {
40
40
  readonly retryAfter: number | null
41
41
  readonly cause: unknown
42
42
  }) {
43
- super(`Email send failed: ${args.provider} ${args.providerCode}`, { cause: args.cause })
43
+ super(`Email request failed: ${args.provider} ${args.providerCode}`, { cause: args.cause })
44
44
  this.reason = args.reason
45
45
  this.classification = CLASSIFICATION_BY_REASON[args.reason]
46
46
  this.provider = args.provider
@@ -37,6 +37,7 @@ const EMAIL_TYPES = {
37
37
 
38
38
  const BaseDataSchema = z.object({
39
39
  email_id: z.string().min(1),
40
+ message_id: z.string().min(1),
40
41
  to: z.array(EmailAddressSchema).min(1),
41
42
  subject: z.string(),
42
43
  tags: z.record(z.string(), z.string()).default({}),
@@ -67,6 +68,7 @@ const EmailPayloadSchema = z.discriminatedUnion('type', [
67
68
  type EmailEventBase = {
68
69
  readonly id: string
69
70
  readonly emailId: string
71
+ readonly messageId: string
70
72
  readonly recipients: readonly string[]
71
73
  readonly subject: string
72
74
  readonly tags: Readonly<Record<string, string>>
@@ -100,6 +102,7 @@ export function parseEmailEvent(payload: WebhookEventPayload, id: string): Email
100
102
  const base: EmailEventBase = {
101
103
  id,
102
104
  emailId: event.data.email_id,
105
+ messageId: event.data.message_id,
103
106
  recipients: event.data.to,
104
107
  subject: event.data.subject,
105
108
  tags: event.data.tags,
@@ -1,3 +1,3 @@
1
- export { ResendEmailService, type ResendEmailClient } from './resend-email-service.ts'
2
- export { ResendWebhook, type ResendWebhookClient } from './resend-webhook.ts'
1
+ export { ResendEmailService } from './resend-email-service.ts'
2
+ export { ResendWebhook } from './resend-webhook.ts'
3
3
  export type { EmailEvent, EmailEventType } from './email-event.ts'
@@ -2,7 +2,13 @@ import { Buffer } from 'node:buffer'
2
2
 
3
3
  import type { Attachment, CreateEmailOptions, ErrorResponse, Resend } from 'resend'
4
4
 
5
- import type { EmailAccepted, EmailAttachment, IEmailService, OutboundEmail } from '@/application'
5
+ import type {
6
+ EmailAccepted,
7
+ EmailAttachment,
8
+ IEmailService,
9
+ OutboundEmail,
10
+ ReceivedEmail,
11
+ } from '@/application'
6
12
  import { EmailServiceError, type EmailSendFailureReason } from '@/core'
7
13
 
8
14
  // SDK additions must receive an explicit classification before the package builds.
@@ -67,37 +73,27 @@ function toAttachment(attachment: EmailAttachment): Attachment {
67
73
  return result
68
74
  }
69
75
 
70
- /** The SDK operations required by the adapter; a test factory can return a plain object. */
71
- export type ResendEmailClient = { readonly emails: Pick<Resend['emails'], 'send'> }
72
-
73
76
  /**
74
77
  * Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.
75
78
  * The Resend SDK itself logs API errors outside production.
76
79
  */
77
80
  export class ResendEmailService implements IEmailService {
78
- /** Load the SDK on the first send. Constructing the service does not load it. */
81
+ /** Load the SDK on the first email operation. Constructing the service does not load it. */
79
82
  static create(apiKey: string, fromAddress: string): ResendEmailService {
80
- return new ResendEmailService(
81
- () => import('resend').then(({ Resend }) => new Resend(apiKey)),
82
- fromAddress,
83
- )
83
+ return new ResendEmailService(apiKey, fromAddress)
84
84
  }
85
85
 
86
- private client: Promise<ResendEmailClient> | undefined
86
+ private client: Resend | undefined
87
87
 
88
- /** Supply an async factory. The service calls it once, on the first send, and keeps its promise. */
88
+ /** Store configuration. Load the SDK and create its client on the first email operation. */
89
89
  constructor(
90
- private readonly createClient: () => Promise<ResendEmailClient>,
90
+ private readonly apiKey: string,
91
91
  private readonly fromAddress: string,
92
92
  ) {}
93
93
 
94
94
  /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */
95
95
  async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {
96
- if (this.client === undefined) {
97
- // Cache synchronous factory throws as rejections too, so later sends cannot initialize again.
98
- this.client = Promise.resolve().then(this.createClient)
99
- }
100
- const client = await this.client
96
+ const client = await this.getClient()
101
97
  const payload: CreateEmailOptions = {
102
98
  from: email.from ?? this.fromAddress,
103
99
  to: [email.to].flat(),
@@ -125,4 +121,30 @@ export class ResendEmailService implements IEmailService {
125
121
  }
126
122
  return { providerId: result.data.id }
127
123
  }
124
+
125
+ /** Retrieve headers by the received email's provider ID. Provider failures throw EmailServiceError. */
126
+ async getReceivedEmail(emailId: string): Promise<ReceivedEmail> {
127
+ const client = await this.getClient()
128
+ const result = await client.emails.receiving.get(emailId)
129
+ if (result.error !== null) {
130
+ throw new EmailServiceError({
131
+ reason: reasonFromResend(result.error),
132
+ provider: 'resend',
133
+ providerCode: result.error.name,
134
+ statusCode: result.error.statusCode,
135
+ retryAfter: retryAfterSeconds(result.headers?.['retry-after']),
136
+ cause: result.error,
137
+ })
138
+ }
139
+ return { headers: result.data.headers ?? {} }
140
+ }
141
+
142
+ private async getClient(): Promise<Resend> {
143
+ if (this.client === undefined) {
144
+ const { Resend } = await import('resend')
145
+ // Recheck after the import so concurrent calls share one client.
146
+ this.client ??= new Resend(this.apiKey)
147
+ }
148
+ return this.client
149
+ }
128
150
  }
@@ -9,27 +9,17 @@ const SignatureErrorSchema = z
9
9
  .instanceof(Error)
10
10
  .refine((error) => error.name === 'WebhookVerificationError')
11
11
 
12
- /** The SDK's local signature verifier. It makes no API request. */
13
- export type ResendWebhookClient = Pick<Resend['webhooks'], 'verify'>
14
-
15
12
  /** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */
16
13
  export class ResendWebhook implements WebhookVerifier<EmailEvent> {
17
14
  /** Load the SDK on the first verification. Only the webhook secret is required. */
18
15
  static create(webhookSecret: string): ResendWebhook {
19
- return new ResendWebhook(webhookSecret, async () => {
20
- const { Resend } = await import('resend')
21
- // The SDK requires a nonempty key even though its local verifier never uses that key.
22
- return new Resend('re_webhook_verification_only').webhooks
23
- })
16
+ return new ResendWebhook(webhookSecret)
24
17
  }
25
18
 
26
- private client: Promise<ResendWebhookClient> | undefined
19
+ private client: Resend['webhooks'] | undefined
27
20
 
28
- /** Inject the local SDK verifier when testing. */
29
- constructor(
30
- private readonly webhookSecret: string,
31
- private readonly verifier: () => Promise<ResendWebhookClient>,
32
- ) {}
21
+ /** Store the webhook secret. Load the SDK on the first verification. */
22
+ constructor(private readonly webhookSecret: string) {}
33
23
 
34
24
  /** Verify the raw body and Svix headers. Invalid requests throw typed errors; other failures propagate. */
35
25
  async verify(body: string, headers: Headers): Promise<EmailEvent | null> {
@@ -37,8 +27,8 @@ export class ResendWebhook implements WebhookVerifier<EmailEvent> {
37
27
  const timestamp = headers.get('svix-timestamp')
38
28
  const signature = headers.get('svix-signature')
39
29
  if (!id || !timestamp || !signature) throw new WebhookVerificationError('missing_headers')
40
- const client = await (this.client ??= this.verifier())
41
- let payload: ReturnType<ResendWebhookClient['verify']>
30
+ const client = await this.getClient()
31
+ let payload: ReturnType<Resend['webhooks']['verify']>
42
32
  try {
43
33
  payload = client.verify({
44
34
  payload: body,
@@ -57,4 +47,13 @@ export class ResendWebhook implements WebhookVerifier<EmailEvent> {
57
47
  throw cause
58
48
  }
59
49
  }
50
+
51
+ private async getClient(): Promise<Resend['webhooks']> {
52
+ if (this.client === undefined) {
53
+ const { Resend } = await import('resend')
54
+ // The SDK requires a nonempty key even though its local verifier never uses that key.
55
+ this.client ??= new Resend('re_webhook_verification_only').webhooks
56
+ }
57
+ return this.client
58
+ }
60
59
  }
@@ -0,0 +1,16 @@
1
+ import Stripe from 'stripe'
2
+
3
+ /** Default SDK retries; client configuration and individual requests can override this value. */
4
+ export const STRIPE_REQUEST_OPTIONS = Object.freeze({ maxNetworkRetries: 2 })
5
+
6
+ /**
7
+ * Construct a native Stripe client with fetch transport and two SDK retries by default.
8
+ * Native configuration overrides these defaults. Inject the client into application services.
9
+ */
10
+ export function createStripeClient(secretKey: string, options: Stripe.StripeConfig = {}): Stripe {
11
+ return new Stripe(secretKey, {
12
+ httpClient: Stripe.createFetchHttpClient(),
13
+ ...STRIPE_REQUEST_OPTIONS,
14
+ ...options,
15
+ })
16
+ }
@@ -0,0 +1,25 @@
1
+ import type Stripe from 'stripe'
2
+
3
+ type EventObject<K extends Stripe.Event['type']> = Extract<
4
+ Stripe.Event,
5
+ { type: K }
6
+ >['data']['object']
7
+
8
+ /**
9
+ * Convert verified Stripe events with callbacks typed to each event's object.
10
+ * Unmapped events return undefined. Applications own validation and payment policy.
11
+ */
12
+ export function stripeEventMap<T>(map: {
13
+ [K in Stripe.Event['type']]?: (object: EventObject<K>) => T
14
+ }): (event: Stripe.Event) => T | undefined {
15
+ const handlers: Partial<Record<Stripe.Event['type'], (object: never) => T>> = map
16
+ return (event) => {
17
+ // SAFETY: The map binds each event type to its object; indexed lookup loses that correlation.
18
+ return handlers[event.type]?.(event.data.object as never)
19
+ }
20
+ }
21
+
22
+ /** Identify one Stripe event delivery; different events for one payment retain different IDs. */
23
+ export function stripeEventId(event: Stripe.Event): string {
24
+ return `stripe:event:${event.id}`
25
+ }
@@ -26,7 +26,8 @@ function classifyStripeError(error: Stripe.errors.StripeError): FailureClassific
26
26
  error instanceof Stripe.errors.StripeAuthenticationError ||
27
27
  error instanceof Stripe.errors.StripePermissionError ||
28
28
  error instanceof Stripe.errors.StripeInvalidRequestError ||
29
- error instanceof Stripe.errors.StripeIdempotencyError
29
+ error instanceof Stripe.errors.StripeIdempotencyError ||
30
+ error instanceof Stripe.errors.StripeCardError
30
31
  )
31
32
  return 'terminal'
32
33
  if (error.headers?.['stripe-should-retry'] === 'true') return 'transient'
@@ -1,6 +1,6 @@
1
- import Stripe from 'stripe'
1
+ import type Stripe from 'stripe'
2
2
 
3
- import { PaymentServiceError } from './payment-service.error.ts'
3
+ import { stripeRequest } from './request.ts'
4
4
 
5
5
  /** Supply the same key and parameters when repeating one durable payment attempt. */
6
6
  export type PaymentRequestOptions = Stripe.RequestOptions &
@@ -24,7 +24,7 @@ export type PaymentResult = Awaited<ReturnType<Stripe['paymentIntents']['create'
24
24
 
25
25
  /**
26
26
  * Stripe payment operations, with native types and stable keys for mutations.
27
- * Rejects with PaymentServiceError or native StripeCardError; unexpected defects propagate.
27
+ * Rejects with PaymentServiceError for Stripe failures; non-Stripe failures propagate unchanged.
28
28
  */
29
29
  export interface IPaymentService {
30
30
  /** Create a customer; the application owns persistence and reuse of the customer link. */
@@ -36,7 +36,7 @@ export interface IPaymentService {
36
36
  createCheckout(input: CheckoutInput, options: PaymentRequestOptions): Promise<CheckoutResult>
37
37
  /** Create checkout in setup mode. The application records consent and verifies the saved method's owner. */
38
38
  setupPaymentMethod(input: CheckoutInput, options: PaymentRequestOptions): Promise<CheckoutResult>
39
- /** Confirm a saved method off-session. Native card errors can require customer authentication. */
39
+ /** Confirm a saved method off-session. PaymentServiceError retains card errors in cause. */
40
40
  chargeSavedMethod(
41
41
  input: SavedMethodCharge,
42
42
  options: PaymentRequestOptions,
@@ -51,7 +51,7 @@ export interface IPaymentService {
51
51
 
52
52
  /**
53
53
  * Ported from Typist's StripeClient request boundary; the native SDK now owns retries.
54
- * Each request defaults to two SDK retries. Native request options can override retries and timeout.
54
+ * The injected client owns retry and timeout defaults. Native request options can override them.
55
55
  */
56
56
  export class StripePaymentService implements IPaymentService {
57
57
  /** Inject a native SDK instance; its transport, credentials, and API version remain caller-owned. */
@@ -62,18 +62,13 @@ export class StripePaymentService implements IPaymentService {
62
62
  input: Stripe.CustomerCreateParams,
63
63
  options: PaymentRequestOptions,
64
64
  ): Promise<CustomerResult> {
65
- return this.request('customers.create', () =>
66
- this.stripe.customers.create(input, { maxNetworkRetries: 2, ...options }),
67
- )
65
+ return stripeRequest('customers.create', () => this.stripe.customers.create(input, options))
68
66
  }
69
67
 
70
68
  /** Force payment mode after caller parameters; return the native Checkout Session. */
71
69
  createCheckout(input: CheckoutInput, options: PaymentRequestOptions): Promise<CheckoutResult> {
72
- return this.request('checkout.sessions.create', () =>
73
- this.stripe.checkout.sessions.create(
74
- { ...input, mode: 'payment' },
75
- { maxNetworkRetries: 2, ...options },
76
- ),
70
+ return stripeRequest('checkout.sessions.create', () =>
71
+ this.stripe.checkout.sessions.create({ ...input, mode: 'payment' }, options),
77
72
  )
78
73
  }
79
74
 
@@ -82,24 +77,18 @@ export class StripePaymentService implements IPaymentService {
82
77
  input: CheckoutInput,
83
78
  options: PaymentRequestOptions,
84
79
  ): Promise<CheckoutResult> {
85
- return this.request('checkout.sessions.create', () =>
86
- this.stripe.checkout.sessions.create(
87
- { ...input, mode: 'setup' },
88
- { maxNetworkRetries: 2, ...options },
89
- ),
80
+ return stripeRequest('checkout.sessions.create', () =>
81
+ this.stripe.checkout.sessions.create({ ...input, mode: 'setup' }, options),
90
82
  )
91
83
  }
92
84
 
93
- /** Force confirmation and off-session use; preserve native card failures for application handling. */
85
+ /** Force confirmation and off-session use; retain native card failures in PaymentServiceError.cause. */
94
86
  chargeSavedMethod(
95
87
  input: SavedMethodCharge,
96
88
  options: PaymentRequestOptions,
97
89
  ): Promise<PaymentResult> {
98
- return this.request('paymentIntents.create', () =>
99
- this.stripe.paymentIntents.create(
100
- { ...input, confirm: true, off_session: true },
101
- { maxNetworkRetries: 2, ...options },
102
- ),
90
+ return stripeRequest('paymentIntents.create', () =>
91
+ this.stripe.paymentIntents.create({ ...input, confirm: true, off_session: true }, options),
103
92
  )
104
93
  }
105
94
 
@@ -109,21 +98,8 @@ export class StripePaymentService implements IPaymentService {
109
98
  params?: Stripe.PaymentIntentRetrieveParams,
110
99
  options?: Stripe.RequestOptions,
111
100
  ): Promise<PaymentResult> {
112
- return this.request('paymentIntents.retrieve', () =>
113
- this.stripe.paymentIntents.retrieve(paymentId, params, { maxNetworkRetries: 2, ...options }),
101
+ return stripeRequest('paymentIntents.retrieve', () =>
102
+ this.stripe.paymentIntents.retrieve(paymentId, params, options),
114
103
  )
115
104
  }
116
-
117
- private async request<T>(operation: string, work: () => Promise<T>): Promise<T> {
118
- try {
119
- return await work()
120
- } catch (cause) {
121
- if (
122
- !(cause instanceof Stripe.errors.StripeError) ||
123
- cause instanceof Stripe.errors.StripeCardError
124
- )
125
- throw cause
126
- throw new PaymentServiceError(operation, cause)
127
- }
128
- }
129
105
  }