@easypayment/medusa-payment-paypal 0.9.15 → 0.9.16

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 (74) hide show
  1. package/.medusa/server/src/admin/index.js +17 -17
  2. package/.medusa/server/src/admin/index.mjs +17 -17
  3. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
  4. package/.medusa/server/src/api/store/paypal/capture-order/route.js +21 -10
  5. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  6. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  7. package/.medusa/server/src/api/store/paypal/create-order/route.js +76 -54
  8. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  9. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
  10. package/.medusa/server/src/api/store/paypal/webhook/route.js +2 -1
  11. package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
  12. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
  13. package/.medusa/server/src/api/store/paypal-complete/route.js +29 -27
  14. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
  15. package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -1
  16. package/.medusa/server/src/jobs/paypal-webhook-retry.js +42 -1
  17. package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
  18. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.d.ts.map +1 -1
  19. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js +5 -4
  20. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js.map +1 -1
  21. package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts.map +1 -1
  22. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js +13 -10
  23. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js.map +1 -1
  24. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
  25. package/.medusa/server/src/modules/paypal/payment-provider/service.js +10 -9
  26. package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
  27. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts +12 -0
  28. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts.map +1 -1
  29. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js +12 -0
  30. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js.map +1 -1
  31. package/.medusa/server/src/modules/paypal/service.d.ts +10 -1
  32. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  33. package/.medusa/server/src/modules/paypal/service.js +71 -21
  34. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  35. package/.medusa/server/src/modules/paypal/utils/amounts.d.ts +9 -1
  36. package/.medusa/server/src/modules/paypal/utils/amounts.d.ts.map +1 -1
  37. package/.medusa/server/src/modules/paypal/utils/amounts.js +17 -5
  38. package/.medusa/server/src/modules/paypal/utils/amounts.js.map +1 -1
  39. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +0 -8
  40. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -1
  41. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +0 -22
  42. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -1
  43. package/.medusa/server/src/modules/paypal/utils/paypal-fetch.d.ts +16 -0
  44. package/.medusa/server/src/modules/paypal/utils/paypal-fetch.d.ts.map +1 -0
  45. package/.medusa/server/src/modules/paypal/utils/paypal-fetch.js +31 -0
  46. package/.medusa/server/src/modules/paypal/utils/paypal-fetch.js.map +1 -0
  47. package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts +14 -0
  48. package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts.map +1 -0
  49. package/.medusa/server/src/modules/paypal/utils/secret-crypto.js +91 -0
  50. package/.medusa/server/src/modules/paypal/utils/secret-crypto.js.map +1 -0
  51. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
  52. package/.medusa/server/src/modules/paypal/webhook-processor.js +29 -13
  53. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
  54. package/.medusa/server/src/subscribers/paypal-order-invoice.d.ts.map +1 -1
  55. package/.medusa/server/src/subscribers/paypal-order-invoice.js +5 -3
  56. package/.medusa/server/src/subscribers/paypal-order-invoice.js.map +1 -1
  57. package/README.md +12 -0
  58. package/package.json +5 -2
  59. package/src/api/store/paypal/capture-order/route.ts +24 -13
  60. package/src/api/store/paypal/create-order/route.ts +93 -62
  61. package/src/api/store/paypal/webhook/route.ts +2 -1
  62. package/src/api/store/paypal-complete/route.ts +32 -38
  63. package/src/jobs/paypal-webhook-retry.ts +51 -1
  64. package/src/modules/paypal/clients/paypal-seller.client.ts +6 -4
  65. package/src/modules/paypal/payment-provider/card-service.ts +13 -10
  66. package/src/modules/paypal/payment-provider/service.ts +10 -9
  67. package/src/modules/paypal/payment-provider/webhook-utils.ts +12 -0
  68. package/src/modules/paypal/service.ts +76 -21
  69. package/src/modules/paypal/utils/amounts.ts +17 -8
  70. package/src/modules/paypal/utils/paypal-auth.ts +0 -27
  71. package/src/modules/paypal/utils/paypal-fetch.ts +30 -0
  72. package/src/modules/paypal/utils/secret-crypto.ts +94 -0
  73. package/src/modules/paypal/webhook-processor.ts +32 -15
  74. package/src/subscribers/paypal-order-invoice.ts +6 -4
@@ -0,0 +1,94 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto"
2
+
3
+ /**
4
+ * Opt-in encryption-at-rest for PayPal secrets (client secret, app access
5
+ * token).
6
+ *
7
+ * Activated only when `PAYPAL_ENCRYPTION_KEY` is set. When it is unset (the
8
+ * default), both functions are the identity — behavior is byte-for-byte
9
+ * unchanged, so existing deployments are unaffected.
10
+ *
11
+ * Format: `enc:v1:<iv_b64>:<tag_b64>:<ciphertext_b64>` using AES-256-GCM
12
+ * (authenticated, so tampering is detected on decrypt). Legacy plaintext values
13
+ * (no prefix) are returned as-is on decrypt, so secrets stored before
14
+ * encryption was enabled keep working and are upgraded on the next save.
15
+ */
16
+ const PREFIX = "enc:v1:"
17
+
18
+ let cachedRaw: string | undefined
19
+ let cachedKey: Buffer | null = null
20
+
21
+ /**
22
+ * `@types/node` 22.0-22.6 typed `Buffer` as `Buffer<ArrayBufferLike>`, which is
23
+ * rejected where the `crypto` APIs and `Buffer.concat` expect a plain
24
+ * `Uint8Array` (the regression was fixed in 22.8). Narrowing each byte view to
25
+ * `Uint8Array` at the call boundary sidesteps it without changing runtime
26
+ * behavior, and stays valid on every other `@types/node` / TypeScript version.
27
+ */
28
+ const bytes = (view: Buffer): Uint8Array => view as unknown as Uint8Array
29
+
30
+ function getKey(): Buffer | null {
31
+ const raw = (process.env.PAYPAL_ENCRYPTION_KEY || "").trim()
32
+ if (raw !== cachedRaw) {
33
+ cachedRaw = raw
34
+ // Accept a key of any length/format; derive a stable 32-byte AES key.
35
+ cachedKey = raw ? createHash("sha256").update(raw).digest() : null
36
+ }
37
+ return cachedKey
38
+ }
39
+
40
+ export function isSecretEncryptionEnabled(): boolean {
41
+ return getKey() !== null
42
+ }
43
+
44
+ export function isEncrypted(value: unknown): boolean {
45
+ return typeof value === "string" && value.startsWith(PREFIX)
46
+ }
47
+
48
+ /**
49
+ * Encrypt a secret for storage. Returns the input unchanged when no key is
50
+ * configured or when the value is empty/already encrypted.
51
+ */
52
+ export function encryptSecret<T extends string | null | undefined>(value: T): T | string {
53
+ if (!value) {
54
+ return value
55
+ }
56
+ const key = getKey()
57
+ if (!key || isEncrypted(value)) {
58
+ return value
59
+ }
60
+ const iv = randomBytes(12)
61
+ const cipher = createCipheriv("aes-256-gcm", bytes(key), bytes(iv))
62
+ const ciphertext = Buffer.concat([bytes(cipher.update(String(value), "utf8")), bytes(cipher.final())])
63
+ const tag = cipher.getAuthTag()
64
+ return `${PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ciphertext.toString("base64")}`
65
+ }
66
+
67
+ /**
68
+ * Decrypt a stored secret. Returns non-encrypted (legacy plaintext) values
69
+ * unchanged. Throws if the value is encrypted but no key is configured, or if
70
+ * authentication fails (wrong key / tampering).
71
+ */
72
+ export function decryptSecret<T extends string | null | undefined>(value: T): T | string {
73
+ if (!value || !isEncrypted(value)) {
74
+ return value
75
+ }
76
+ const key = getKey()
77
+ if (!key) {
78
+ throw new Error(
79
+ "PayPal secret is encrypted but PAYPAL_ENCRYPTION_KEY is not set. Restore the key to decrypt stored credentials."
80
+ )
81
+ }
82
+ const body = String(value).slice(PREFIX.length)
83
+ const [ivB64, tagB64, ctB64] = body.split(":")
84
+ if (!ivB64 || !tagB64 || !ctB64) {
85
+ throw new Error("PayPal secret ciphertext is malformed.")
86
+ }
87
+ const decipher = createDecipheriv("aes-256-gcm", bytes(key), bytes(Buffer.from(ivB64, "base64")))
88
+ decipher.setAuthTag(bytes(Buffer.from(tagB64, "base64")))
89
+ const plaintext = Buffer.concat([
90
+ bytes(decipher.update(bytes(Buffer.from(ctB64, "base64")))),
91
+ bytes(decipher.final()),
92
+ ])
93
+ return plaintext.toString("utf8")
94
+ }
@@ -1,6 +1,6 @@
1
1
  import type { MedusaContainer } from "@medusajs/framework/types"
2
2
  import { Modules } from "@medusajs/framework/utils"
3
- import { isPayPalProviderId } from "./utils/provider-ids"
3
+ import { isPayPalProviderId, PAYPAL_PROVIDER_IDS } from "./utils/provider-ids"
4
4
 
5
5
 
6
6
  export const EVENT_STATUS_MAP: Record<
@@ -143,7 +143,10 @@ export function extractIdentifiers(
143
143
  refundId = String(resource?.id || "").trim() || null
144
144
  orderId = String(related?.order_id || "").trim() || null
145
145
  captureId = String(related?.capture_id || "").trim() || null
146
- cartId = null
146
+ // The refund resource carries the capture's custom_id (the cart id) when it
147
+ // was set on the purchase unit — use it so refunds resolve directly instead
148
+ // of always falling back to a session scan.
149
+ cartId = String(resource?.custom_id || "").trim() || null
147
150
  }
148
151
 
149
152
  return { orderId, captureId, refundId, cartId }
@@ -294,21 +297,35 @@ export async function processPayPalWebhookEvent(
294
297
 
295
298
  let cartId = rawCartId
296
299
 
297
- if (!cartId) {
300
+ if (!cartId && (orderId || captureId)) {
298
301
  try {
299
302
  const paymentModule = container.resolve(Modules.PAYMENT) as any
300
- const allSessions = await paymentModule.listPaymentSessions(
301
- {
302
- provider_id: ["pp_paypal_paypal", "pp_paypal_card_paypal_card"],
303
- },
304
- { take: 200 }
305
- )
306
- const matchedSession = (allSessions || []).find((s: Record<string, unknown>) => {
307
- const pp = ((s.data || {}) as Record<string, unknown>).paypal as Record<string, unknown> || {}
308
- if (orderId && pp.order_id === orderId) return true
309
- if (captureId && pp.capture_id === captureId) return true
310
- return false
311
- })
303
+ // Fallback when the webhook resource carries no custom_id: locate the
304
+ // PayPal session whose stored data matches this order_id / capture_id.
305
+ // Paginate to exhaustion rather than scanning a single fixed page — on a
306
+ // busy store the matching session is frequently beyond the first page, so
307
+ // a hard cap would silently drop capture/refund status updates.
308
+ const PAGE_SIZE = 200
309
+ const MAX_PAGES = 100 // safety bound (20k sessions) against a misbehaving driver
310
+ let matchedSession: Record<string, unknown> | null = null
311
+
312
+ for (let page = 0; page < MAX_PAGES && !matchedSession; page++) {
313
+ const sessions = await paymentModule.listPaymentSessions(
314
+ { provider_id: [...PAYPAL_PROVIDER_IDS] },
315
+ { take: PAGE_SIZE, skip: page * PAGE_SIZE }
316
+ )
317
+ if (!sessions || sessions.length === 0) break
318
+ matchedSession =
319
+ sessions.find((s: Record<string, unknown>) => {
320
+ const pp =
321
+ (((s.data || {}) as Record<string, unknown>).paypal as Record<string, unknown>) || {}
322
+ if (orderId && pp.order_id === orderId) return true
323
+ if (captureId && pp.capture_id === captureId) return true
324
+ return false
325
+ }) || null
326
+ if (sessions.length < PAGE_SIZE) break
327
+ }
328
+
312
329
  if (matchedSession?.payment_collection_id) {
313
330
  const colls = await paymentModule.listPaymentCollections(
314
331
  { id: [matchedSession.payment_collection_id] },
@@ -1,6 +1,7 @@
1
1
  import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
2
2
  import type PayPalModuleService from "../modules/paypal/service"
3
- import { getPayPalAccessToken } from "../modules/paypal/utils/paypal-auth"
3
+ import { getPayPalApiBase } from "../modules/paypal/utils/paypal-auth"
4
+ import { paypalFetch } from "../modules/paypal/utils/paypal-fetch"
4
5
  import { isPayPalProviderId } from "../modules/paypal/utils/provider-ids"
5
6
 
6
7
  const PATCHABLE_STATUSES = new Set(["CREATED", "APPROVED", "SAVED"])
@@ -83,12 +84,13 @@ export default async function paypalOrderInvoiceHandler({
83
84
  if (!invoiceId) return
84
85
 
85
86
  const creds = await paypal.getActiveCredentials()
86
- const { accessToken, base } = await getPayPalAccessToken(creds)
87
+ const base = getPayPalApiBase(creds.environment)
88
+ const accessToken = await paypal.getAppAccessToken()
87
89
 
88
90
  let paypalOrderStatus = ""
89
91
  let currentInvoiceId = ""
90
92
  try {
91
- const statusResp = await fetch(
93
+ const statusResp = await paypalFetch(
92
94
  `${base}/v2/checkout/orders/${encodeURIComponent(paypalOrderId)}`,
93
95
  { headers: { Authorization: `Bearer ${accessToken}` } }
94
96
  )
@@ -128,7 +130,7 @@ export default async function paypalOrderInvoiceHandler({
128
130
  return
129
131
  }
130
132
 
131
- const patchResp = await fetch(
133
+ const patchResp = await paypalFetch(
132
134
  `${base}/v2/checkout/orders/${encodeURIComponent(paypalOrderId)}`,
133
135
  {
134
136
  method: "PATCH",