@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.
- package/.medusa/server/src/admin/index.js +17 -17
- package/.medusa/server/src/admin/index.mjs +17 -17
- package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/capture-order/route.js +21 -10
- package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/create-order/route.js +76 -54
- package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/webhook/route.js +2 -1
- package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal-complete/route.js +29 -27
- package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
- package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -1
- package/.medusa/server/src/jobs/paypal-webhook-retry.js +42 -1
- package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
- package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js +5 -4
- package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.js +13 -10
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/service.js +10 -9
- package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts +12 -0
- package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js +12 -0
- package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js.map +1 -1
- package/.medusa/server/src/modules/paypal/service.d.ts +10 -1
- package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/service.js +71 -21
- package/.medusa/server/src/modules/paypal/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/amounts.d.ts +9 -1
- package/.medusa/server/src/modules/paypal/utils/amounts.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/amounts.js +17 -5
- package/.medusa/server/src/modules/paypal/utils/amounts.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +0 -8
- package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +0 -22
- package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/paypal-fetch.d.ts +16 -0
- package/.medusa/server/src/modules/paypal/utils/paypal-fetch.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/paypal-fetch.js +31 -0
- package/.medusa/server/src/modules/paypal/utils/paypal-fetch.js.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts +14 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.js +91 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.js.map +1 -0
- package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/webhook-processor.js +29 -13
- package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
- package/.medusa/server/src/subscribers/paypal-order-invoice.d.ts.map +1 -1
- package/.medusa/server/src/subscribers/paypal-order-invoice.js +5 -3
- package/.medusa/server/src/subscribers/paypal-order-invoice.js.map +1 -1
- package/README.md +12 -0
- package/package.json +5 -2
- package/src/api/store/paypal/capture-order/route.ts +24 -13
- package/src/api/store/paypal/create-order/route.ts +93 -62
- package/src/api/store/paypal/webhook/route.ts +2 -1
- package/src/api/store/paypal-complete/route.ts +32 -38
- package/src/jobs/paypal-webhook-retry.ts +51 -1
- package/src/modules/paypal/clients/paypal-seller.client.ts +6 -4
- package/src/modules/paypal/payment-provider/card-service.ts +13 -10
- package/src/modules/paypal/payment-provider/service.ts +10 -9
- package/src/modules/paypal/payment-provider/webhook-utils.ts +12 -0
- package/src/modules/paypal/service.ts +76 -21
- package/src/modules/paypal/utils/amounts.ts +17 -8
- package/src/modules/paypal/utils/paypal-auth.ts +0 -27
- package/src/modules/paypal/utils/paypal-fetch.ts +30 -0
- package/src/modules/paypal/utils/secret-crypto.ts +94 -0
- package/src/modules/paypal/webhook-processor.ts +32 -15
- 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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
133
|
+
const patchResp = await paypalFetch(
|
|
132
134
|
`${base}/v2/checkout/orders/${encodeURIComponent(paypalOrderId)}`,
|
|
133
135
|
{
|
|
134
136
|
method: "PATCH",
|