@easypayment/medusa-payment-paypal 0.9.16 → 0.9.18
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 +59 -32
- package/.medusa/server/src/admin/index.mjs +59 -32
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts +4 -1
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts.map +1 -1
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js +64 -2
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js.map +1 -1
- package/.medusa/server/src/api/middlewares.d.ts.map +1 -1
- package/.medusa/server/src/api/middlewares.js +9 -0
- package/.medusa/server/src/api/middlewares.js.map +1 -1
- 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 +8 -58
- 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 +17 -43
- 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 +13 -8
- package/.medusa/server/src/api/store/paypal/webhook/route.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 +54 -31
- package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/status-utils.d.ts +32 -0
- package/.medusa/server/src/modules/paypal/payment-provider/status-utils.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/payment-provider/status-utils.js +65 -0
- package/.medusa/server/src/modules/paypal/payment-provider/status-utils.js.map +1 -0
- package/.medusa/server/src/modules/paypal/service.d.ts +12 -1
- package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/service.js +14 -3
- package/.medusa/server/src/modules/paypal/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/payment-session.d.ts +35 -0
- package/.medusa/server/src/modules/paypal/utils/payment-session.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/payment-session.js +78 -0
- package/.medusa/server/src/modules/paypal/utils/payment-session.js.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts +2 -1
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.js +82 -26
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/webhook-verify.d.ts +28 -0
- package/.medusa/server/src/modules/paypal/utils/webhook-verify.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/webhook-verify.js +58 -0
- package/.medusa/server/src/modules/paypal/utils/webhook-verify.js.map +1 -0
- package/package.json +1 -1
- package/src/admin/routes/settings/paypal/connection/page.tsx +68 -12
- package/src/api/middlewares.ts +9 -0
- package/src/api/store/paypal/capture-order/route.ts +8 -71
- package/src/api/store/paypal/create-order/route.ts +25 -47
- package/src/api/store/paypal/webhook/route.ts +22 -8
- package/src/modules/paypal/payment-provider/service.ts +73 -29
- package/src/modules/paypal/payment-provider/status-utils.ts +59 -0
- package/src/modules/paypal/service.ts +17 -3
- package/src/modules/paypal/utils/payment-session.ts +104 -0
- package/src/modules/paypal/utils/secret-crypto.ts +90 -27
- package/src/modules/paypal/utils/webhook-verify.ts +61 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findPayPalSessionForCart = findPayPalSessionForCart;
|
|
4
|
+
exports.getStoredPayPalOrderId = getStoredPayPalOrderId;
|
|
5
|
+
exports.updatePayPalSessionData = updatePayPalSessionData;
|
|
6
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
7
|
+
const provider_ids_1 = require("./provider-ids");
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the cart's active PayPal payment session (wallet or card). When a
|
|
10
|
+
* cart has more than one PayPal session, the most recently created one wins —
|
|
11
|
+
* both create-order and capture-order rely on this same selection so they
|
|
12
|
+
* always read and write the same session.
|
|
13
|
+
*/
|
|
14
|
+
async function findPayPalSessionForCart(cartId, scope) {
|
|
15
|
+
try {
|
|
16
|
+
const query = scope.resolve("query");
|
|
17
|
+
const { data: carts } = await query.graph({
|
|
18
|
+
entity: "cart",
|
|
19
|
+
fields: [
|
|
20
|
+
"id",
|
|
21
|
+
"payment_collection.payment_sessions.id",
|
|
22
|
+
"payment_collection.payment_sessions.data",
|
|
23
|
+
"payment_collection.payment_sessions.status",
|
|
24
|
+
"payment_collection.payment_sessions.provider_id",
|
|
25
|
+
"payment_collection.payment_sessions.created_at",
|
|
26
|
+
],
|
|
27
|
+
filters: { id: cartId },
|
|
28
|
+
});
|
|
29
|
+
const cart = carts?.[0];
|
|
30
|
+
const sessions = cart?.payment_collection?.payment_sessions || [];
|
|
31
|
+
const session = sessions
|
|
32
|
+
.filter((s) => (0, provider_ids_1.isPayPalProviderId)(s.provider_id))
|
|
33
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())[0];
|
|
34
|
+
if (!session)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
session_id: session.id,
|
|
38
|
+
session_data: (session.data || {}),
|
|
39
|
+
session_status: session.status,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
console.warn("[PayPal] findPayPalSessionForCart failed:", e?.message);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read the PayPal order id stored on a session's `data`. Accepts both the
|
|
49
|
+
* canonical `data.paypal.order_id` and the legacy top-level `data.order_id`.
|
|
50
|
+
* Returns null when none is present.
|
|
51
|
+
*/
|
|
52
|
+
function getStoredPayPalOrderId(sessionData) {
|
|
53
|
+
const paypal = (sessionData?.paypal || {});
|
|
54
|
+
const id = String(paypal.order_id || sessionData?.order_id || "");
|
|
55
|
+
return id || null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Shallow-merge `extraData` into a payment session's `data`, preserving the
|
|
59
|
+
* session's amount/currency. Callers that update the nested `paypal` object are
|
|
60
|
+
* responsible for spreading its existing keys (see `getStoredPayPalOrderId`).
|
|
61
|
+
*/
|
|
62
|
+
async function updatePayPalSessionData(sessionId, extraData, scope) {
|
|
63
|
+
try {
|
|
64
|
+
const paymentModule = scope.resolve(utils_1.Modules.PAYMENT);
|
|
65
|
+
const [existing] = await paymentModule.listPaymentSessions({ id: [sessionId] }, { take: 1 });
|
|
66
|
+
const mergedData = { ...(existing?.data || {}), ...extraData };
|
|
67
|
+
await paymentModule.updatePaymentSession({
|
|
68
|
+
id: sessionId,
|
|
69
|
+
data: mergedData,
|
|
70
|
+
amount: existing?.amount,
|
|
71
|
+
currency_code: existing?.currency_code,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
console.error("[PayPal] updatePayPalSessionData failed:", e?.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=payment-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-session.js","sourceRoot":"","sources":["../../../../../../src/modules/paypal/utils/payment-session.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,8 @@ export declare function isSecretEncryptionEnabled(): boolean;
|
|
|
2
2
|
export declare function isEncrypted(value: unknown): boolean;
|
|
3
3
|
/**
|
|
4
4
|
* Encrypt a secret for storage. Returns the input unchanged when no key is
|
|
5
|
-
* configured or when the value is empty/already encrypted.
|
|
5
|
+
* configured or when the value is empty/already encrypted. Always writes the
|
|
6
|
+
* current (v2) format.
|
|
6
7
|
*/
|
|
7
8
|
export declare function encryptSecret<T extends string | null | undefined>(value: T): T | string;
|
|
8
9
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-crypto.d.ts","sourceRoot":"","sources":["../../../../../../src/modules/paypal/utils/secret-crypto.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-crypto.d.ts","sourceRoot":"","sources":["../../../../../../src/modules/paypal/utils/secret-crypto.ts"],"names":[],"mappings":"AAuFA,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKnD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CAevF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CA+BvF"}
|
|
@@ -13,14 +13,33 @@ const crypto_1 = require("crypto");
|
|
|
13
13
|
* default), both functions are the identity — behavior is byte-for-byte
|
|
14
14
|
* unchanged, so existing deployments are unaffected.
|
|
15
15
|
*
|
|
16
|
-
* Format: `enc:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Format (current): `enc:v2:<salt_b64>:<iv_b64>:<tag_b64>:<ciphertext_b64>`
|
|
17
|
+
* using AES-256-GCM with a key derived via **scrypt** over the env key and a
|
|
18
|
+
* random per-secret salt. scrypt is a memory-hard KDF, so even a low-entropy
|
|
19
|
+
* passphrase resists brute-force/rainbow-table attacks (the previous unsalted,
|
|
20
|
+
* single-pass SHA-256 did not).
|
|
21
|
+
*
|
|
22
|
+
* Format (legacy): `enc:v1:<iv_b64>:<tag_b64>:<ciphertext_b64>` with an
|
|
23
|
+
* unsalted SHA-256 key. Still decryptable so secrets written before the KDF
|
|
24
|
+
* upgrade keep working; they are transparently upgraded to v2 on the next save.
|
|
25
|
+
*
|
|
26
|
+
* Legacy plaintext values (no prefix) are returned as-is on decrypt.
|
|
20
27
|
*/
|
|
21
|
-
const
|
|
28
|
+
const PREFIX_V2 = "enc:v2:";
|
|
29
|
+
const PREFIX_V1 = "enc:v1:";
|
|
30
|
+
// scrypt cost parameters. N=2^14 with r=8 needs ~16MB (128*N*r), which stays
|
|
31
|
+
// under Node's default 32MB scrypt memory limit while imposing a meaningful
|
|
32
|
+
// per-derivation cost. Derived keys are cached, so this runs at most once per
|
|
33
|
+
// (env key, salt) pair rather than on every encrypt/decrypt.
|
|
34
|
+
const SCRYPT_N = 16384;
|
|
35
|
+
const SCRYPT_R = 8;
|
|
36
|
+
const SCRYPT_P = 1;
|
|
37
|
+
const KEY_LEN = 32;
|
|
38
|
+
const SALT_LEN = 16;
|
|
22
39
|
let cachedRaw;
|
|
23
|
-
let
|
|
40
|
+
let cachedRawKey = null;
|
|
41
|
+
let legacyV1Key = null;
|
|
42
|
+
const v2KeyCache = new Map();
|
|
24
43
|
/**
|
|
25
44
|
* `@types/node` 22.0-22.6 typed `Buffer` as `Buffer<ArrayBufferLike>`, which is
|
|
26
45
|
* rejected where the `crypto` APIs and `Buffer.concat` expect a plain
|
|
@@ -29,38 +48,68 @@ let cachedKey = null;
|
|
|
29
48
|
* behavior, and stays valid on every other `@types/node` / TypeScript version.
|
|
30
49
|
*/
|
|
31
50
|
const bytes = (view) => view;
|
|
32
|
-
|
|
51
|
+
/** The raw env key (trimmed), or null when encryption is disabled. */
|
|
52
|
+
function getRawKey() {
|
|
33
53
|
const raw = (process.env.PAYPAL_ENCRYPTION_KEY || "").trim();
|
|
34
54
|
if (raw !== cachedRaw) {
|
|
35
55
|
cachedRaw = raw;
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
cachedRawKey = raw || null;
|
|
57
|
+
// The unsalted SHA-256 key is only needed to decrypt legacy v1 values.
|
|
58
|
+
legacyV1Key = raw ? (0, crypto_1.createHash)("sha256").update(raw).digest() : null;
|
|
59
|
+
v2KeyCache.clear();
|
|
60
|
+
}
|
|
61
|
+
return cachedRawKey;
|
|
62
|
+
}
|
|
63
|
+
/** Derive (and cache) the AES key for a v2 secret from the env key + salt. */
|
|
64
|
+
function deriveV2Key(raw, salt) {
|
|
65
|
+
const cacheKey = `${raw}::${salt.toString("base64")}`;
|
|
66
|
+
let key = v2KeyCache.get(cacheKey);
|
|
67
|
+
if (!key) {
|
|
68
|
+
key = (0, crypto_1.scryptSync)(raw, bytes(salt), KEY_LEN, {
|
|
69
|
+
N: SCRYPT_N,
|
|
70
|
+
r: SCRYPT_R,
|
|
71
|
+
p: SCRYPT_P,
|
|
72
|
+
});
|
|
73
|
+
v2KeyCache.set(cacheKey, key);
|
|
38
74
|
}
|
|
39
|
-
return
|
|
75
|
+
return key;
|
|
76
|
+
}
|
|
77
|
+
function gcmDecrypt(key, ivB64, tagB64, ctB64) {
|
|
78
|
+
const decipher = (0, crypto_1.createDecipheriv)("aes-256-gcm", bytes(key), bytes(Buffer.from(ivB64, "base64")));
|
|
79
|
+
decipher.setAuthTag(bytes(Buffer.from(tagB64, "base64")));
|
|
80
|
+
const plaintext = Buffer.concat([
|
|
81
|
+
bytes(decipher.update(bytes(Buffer.from(ctB64, "base64")))),
|
|
82
|
+
bytes(decipher.final()),
|
|
83
|
+
]);
|
|
84
|
+
return plaintext.toString("utf8");
|
|
40
85
|
}
|
|
41
86
|
function isSecretEncryptionEnabled() {
|
|
42
|
-
return
|
|
87
|
+
return getRawKey() !== null;
|
|
43
88
|
}
|
|
44
89
|
function isEncrypted(value) {
|
|
45
|
-
return typeof value === "string" &&
|
|
90
|
+
return (typeof value === "string" &&
|
|
91
|
+
(value.startsWith(PREFIX_V2) || value.startsWith(PREFIX_V1)));
|
|
46
92
|
}
|
|
47
93
|
/**
|
|
48
94
|
* Encrypt a secret for storage. Returns the input unchanged when no key is
|
|
49
|
-
* configured or when the value is empty/already encrypted.
|
|
95
|
+
* configured or when the value is empty/already encrypted. Always writes the
|
|
96
|
+
* current (v2) format.
|
|
50
97
|
*/
|
|
51
98
|
function encryptSecret(value) {
|
|
52
99
|
if (!value) {
|
|
53
100
|
return value;
|
|
54
101
|
}
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
102
|
+
const raw = getRawKey();
|
|
103
|
+
if (!raw || isEncrypted(value)) {
|
|
57
104
|
return value;
|
|
58
105
|
}
|
|
106
|
+
const salt = (0, crypto_1.randomBytes)(SALT_LEN);
|
|
107
|
+
const key = deriveV2Key(raw, salt);
|
|
59
108
|
const iv = (0, crypto_1.randomBytes)(12);
|
|
60
109
|
const cipher = (0, crypto_1.createCipheriv)("aes-256-gcm", bytes(key), bytes(iv));
|
|
61
110
|
const ciphertext = Buffer.concat([bytes(cipher.update(String(value), "utf8")), bytes(cipher.final())]);
|
|
62
111
|
const tag = cipher.getAuthTag();
|
|
63
|
-
return `${
|
|
112
|
+
return `${PREFIX_V2}${salt.toString("base64")}:${iv.toString("base64")}:${tag.toString("base64")}:${ciphertext.toString("base64")}`;
|
|
64
113
|
}
|
|
65
114
|
/**
|
|
66
115
|
* Decrypt a stored secret. Returns non-encrypted (legacy plaintext) values
|
|
@@ -71,21 +120,28 @@ function decryptSecret(value) {
|
|
|
71
120
|
if (!value || !isEncrypted(value)) {
|
|
72
121
|
return value;
|
|
73
122
|
}
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
123
|
+
const raw = getRawKey();
|
|
124
|
+
if (!raw) {
|
|
76
125
|
throw new Error("PayPal secret is encrypted but PAYPAL_ENCRYPTION_KEY is not set. Restore the key to decrypt stored credentials.");
|
|
77
126
|
}
|
|
78
|
-
|
|
127
|
+
if (String(value).startsWith(PREFIX_V2)) {
|
|
128
|
+
const body = String(value).slice(PREFIX_V2.length);
|
|
129
|
+
const [saltB64, ivB64, tagB64, ctB64] = body.split(":");
|
|
130
|
+
if (!saltB64 || !ivB64 || !tagB64 || !ctB64) {
|
|
131
|
+
throw new Error("PayPal secret ciphertext is malformed.");
|
|
132
|
+
}
|
|
133
|
+
const key = deriveV2Key(raw, Buffer.from(saltB64, "base64"));
|
|
134
|
+
return gcmDecrypt(key, ivB64, tagB64, ctB64);
|
|
135
|
+
}
|
|
136
|
+
// Legacy v1 (unsalted SHA-256 key).
|
|
137
|
+
const body = String(value).slice(PREFIX_V1.length);
|
|
79
138
|
const [ivB64, tagB64, ctB64] = body.split(":");
|
|
80
139
|
if (!ivB64 || !tagB64 || !ctB64) {
|
|
81
140
|
throw new Error("PayPal secret ciphertext is malformed.");
|
|
82
141
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
bytes(decipher.final()),
|
|
88
|
-
]);
|
|
89
|
-
return plaintext.toString("utf8");
|
|
142
|
+
if (!legacyV1Key) {
|
|
143
|
+
throw new Error("PayPal secret is encrypted but PAYPAL_ENCRYPTION_KEY is not set.");
|
|
144
|
+
}
|
|
145
|
+
return gcmDecrypt(legacyV1Key, ivB64, tagB64, ctB64);
|
|
90
146
|
}
|
|
91
147
|
//# sourceMappingURL=secret-crypto.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-crypto.js","sourceRoot":"","sources":["../../../../../../src/modules/paypal/utils/secret-crypto.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"secret-crypto.js","sourceRoot":"","sources":["../../../../../../src/modules/paypal/utils/secret-crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for calling PayPal's `verify-webhook-signature` API correctly.
|
|
3
|
+
*
|
|
4
|
+
* PayPal computes the expected signature over the EXACT bytes it transmitted.
|
|
5
|
+
* Re-serializing the parsed body (`JSON.stringify(req.body)`) can change those
|
|
6
|
+
* bytes — key ordering, non-ASCII escaping (`José` vs `José`), number and
|
|
7
|
+
* whitespace formatting — which makes verification intermittently return
|
|
8
|
+
* FAILURE for legitimate webhooks. When the original raw body is available we
|
|
9
|
+
* inject it verbatim as `webhook_event`; otherwise we fall back to serializing
|
|
10
|
+
* the parsed body so behavior never regresses.
|
|
11
|
+
*/
|
|
12
|
+
/** Normalize Medusa's `req.rawBody` (a Buffer when `preserveRawBody` is set) to a string. */
|
|
13
|
+
export declare function rawBodyToString(rawBody: unknown): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Compose the JSON request body for `verify-webhook-signature`.
|
|
16
|
+
*
|
|
17
|
+
* `fields` are the transmission headers + webhook_id; `webhookEventJson` is the
|
|
18
|
+
* raw webhook body verbatim (preferred) or a serialized fallback. Undefined /
|
|
19
|
+
* null header values are omitted. The webhook event is concatenated as raw JSON
|
|
20
|
+
* (not re-encoded) so the bytes PayPal hashes match what it signed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function composeVerifyRequestBody(fields: Record<string, string | undefined | null>, webhookEventJson: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Pick the bytes to use for `webhook_event`: the raw body when present and
|
|
25
|
+
* non-blank, otherwise a JSON serialization of the parsed body.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveWebhookEventJson(rawBody: string | null | undefined, parsedBody: unknown): string;
|
|
28
|
+
//# sourceMappingURL=webhook-verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-verify.d.ts","sourceRoot":"","sources":["../../../../../../src/modules/paypal/utils/webhook-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAc/D;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC,EACjD,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAMR;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,UAAU,EAAE,OAAO,GAClB,MAAM,CAKR"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Helpers for calling PayPal's `verify-webhook-signature` API correctly.
|
|
4
|
+
*
|
|
5
|
+
* PayPal computes the expected signature over the EXACT bytes it transmitted.
|
|
6
|
+
* Re-serializing the parsed body (`JSON.stringify(req.body)`) can change those
|
|
7
|
+
* bytes — key ordering, non-ASCII escaping (`José` vs `José`), number and
|
|
8
|
+
* whitespace formatting — which makes verification intermittently return
|
|
9
|
+
* FAILURE for legitimate webhooks. When the original raw body is available we
|
|
10
|
+
* inject it verbatim as `webhook_event`; otherwise we fall back to serializing
|
|
11
|
+
* the parsed body so behavior never regresses.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.rawBodyToString = rawBodyToString;
|
|
15
|
+
exports.composeVerifyRequestBody = composeVerifyRequestBody;
|
|
16
|
+
exports.resolveWebhookEventJson = resolveWebhookEventJson;
|
|
17
|
+
/** Normalize Medusa's `req.rawBody` (a Buffer when `preserveRawBody` is set) to a string. */
|
|
18
|
+
function rawBodyToString(rawBody) {
|
|
19
|
+
if (rawBody == null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (typeof rawBody === "string") {
|
|
23
|
+
return rawBody.length > 0 ? rawBody : null;
|
|
24
|
+
}
|
|
25
|
+
if (Buffer.isBuffer(rawBody)) {
|
|
26
|
+
return rawBody.length > 0 ? rawBody.toString("utf8") : null;
|
|
27
|
+
}
|
|
28
|
+
if (rawBody instanceof Uint8Array) {
|
|
29
|
+
return rawBody.length > 0 ? Buffer.from(rawBody).toString("utf8") : null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compose the JSON request body for `verify-webhook-signature`.
|
|
35
|
+
*
|
|
36
|
+
* `fields` are the transmission headers + webhook_id; `webhookEventJson` is the
|
|
37
|
+
* raw webhook body verbatim (preferred) or a serialized fallback. Undefined /
|
|
38
|
+
* null header values are omitted. The webhook event is concatenated as raw JSON
|
|
39
|
+
* (not re-encoded) so the bytes PayPal hashes match what it signed.
|
|
40
|
+
*/
|
|
41
|
+
function composeVerifyRequestBody(fields, webhookEventJson) {
|
|
42
|
+
const head = Object.entries(fields)
|
|
43
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
44
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${JSON.stringify(v)}`)
|
|
45
|
+
.join(",");
|
|
46
|
+
return `{${head}${head ? "," : ""}"webhook_event":${webhookEventJson}}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Pick the bytes to use for `webhook_event`: the raw body when present and
|
|
50
|
+
* non-blank, otherwise a JSON serialization of the parsed body.
|
|
51
|
+
*/
|
|
52
|
+
function resolveWebhookEventJson(rawBody, parsedBody) {
|
|
53
|
+
if (typeof rawBody === "string" && rawBody.trim().length > 0) {
|
|
54
|
+
return rawBody;
|
|
55
|
+
}
|
|
56
|
+
return JSON.stringify(parsedBody ?? {});
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=webhook-verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-verify.js","sourceRoot":"","sources":["../../../../../../src/modules/paypal/utils/webhook-verify.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AAGH,0CAcC;AAUD,4DASC;AAMD,0DAQC;AAhDD,6FAA6F;AAC7F,SAAgB,eAAe,CAAC,OAAgB;IAC9C,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IAC5C,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC7D,CAAC;IACD,IAAI,OAAO,YAAY,UAAU,EAAE,CAAC;QAClC,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1E,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,wBAAwB,CACtC,MAAiD,EACjD,gBAAwB;IAExB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;SAChC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,CAAC;SAChD,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5D,IAAI,CAAC,GAAG,CAAC,CAAA;IACZ,OAAO,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,mBAAmB,gBAAgB,GAAG,CAAA;AACzE,CAAC;AAED;;;GAGG;AACH,SAAgB,uBAAuB,CACrC,OAAkC,EAClC,UAAmB;IAEnB,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7D,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,IAAI,EAAE,CAAC,CAAA;AACzC,CAAC"}
|
package/package.json
CHANGED
|
@@ -24,8 +24,9 @@ declare global {
|
|
|
24
24
|
PAYPAL?: {
|
|
25
25
|
apps?: {
|
|
26
26
|
Signup?: {
|
|
27
|
-
miniBrowser?: { init
|
|
28
|
-
MiniBrowser?: { closeFlow?: () => void }
|
|
27
|
+
miniBrowser?: { init?: () => void; closeFlow?: () => void }
|
|
28
|
+
MiniBrowser?: { init?: () => void; closeFlow?: () => void }
|
|
29
|
+
render?: () => void
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -158,22 +159,86 @@ export default function PayPalConnectionPage() {
|
|
|
158
159
|
if (connState !== "ready" || !finalUrl) return
|
|
159
160
|
|
|
160
161
|
const scriptUrl = PARTNER_JS_URLS[env]
|
|
162
|
+
let cancelled = false
|
|
163
|
+
let pollTimer: ReturnType<typeof setTimeout> | undefined
|
|
164
|
+
|
|
165
|
+
// PayPal's partner.js wires the mini-browser handler to its own
|
|
166
|
+
// DOMContentLoaded listener. Inside the Medusa admin (a React SPA) that
|
|
167
|
+
// event fired long before this route mounted, so partner.js never scans the
|
|
168
|
+
// button and the click falls through to the plain target="_blank" anchor —
|
|
169
|
+
// which opens PayPal onboarding in a NEW TAB instead of the popup.
|
|
170
|
+
//
|
|
171
|
+
// To fix this we load partner.js only after the anchor exists with its real
|
|
172
|
+
// href, then explicitly run PayPal's initializer so it discovers the button
|
|
173
|
+
// and intercepts the click into the mini-browser popup.
|
|
174
|
+
const initPartner = (attempt = 0) => {
|
|
175
|
+
if (cancelled) return
|
|
176
|
+
|
|
177
|
+
const signup = window.PAYPAL?.apps?.Signup
|
|
178
|
+
const target =
|
|
179
|
+
signup?.miniBrowser && typeof signup.miniBrowser.init === "function"
|
|
180
|
+
? signup.miniBrowser
|
|
181
|
+
: signup?.MiniBrowser && typeof signup.MiniBrowser.init === "function"
|
|
182
|
+
? signup.MiniBrowser
|
|
183
|
+
: null
|
|
184
|
+
|
|
185
|
+
if (target?.init) {
|
|
186
|
+
try {
|
|
187
|
+
target.init()
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error("[paypal] partner.js init failed:", e)
|
|
190
|
+
}
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (typeof signup?.render === "function") {
|
|
195
|
+
try {
|
|
196
|
+
signup.render()
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error("[paypal] partner.js render failed:", e)
|
|
199
|
+
}
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The namespace may not be populated the instant onload fires; retry
|
|
204
|
+
// briefly before giving up.
|
|
205
|
+
if (attempt < 40) {
|
|
206
|
+
pollTimer = setTimeout(() => initPartner(attempt + 1), 50)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Last resort: partner.js exposed no initializer we recognise. Re-fire
|
|
211
|
+
// DOMContentLoaded so any listener it registered runs against the
|
|
212
|
+
// now-present button.
|
|
213
|
+
try {
|
|
214
|
+
document.dispatchEvent(new Event("DOMContentLoaded"))
|
|
215
|
+
} catch {}
|
|
216
|
+
}
|
|
161
217
|
|
|
218
|
+
// Remove any stale copy + namespace so the fresh load re-scans the current
|
|
219
|
+
// button (href/environment may have changed since the last render).
|
|
162
220
|
const existingScript = document.getElementById("paypal-partner-js")
|
|
163
221
|
if (existingScript) {
|
|
164
222
|
existingScript.parentNode?.removeChild(existingScript)
|
|
165
223
|
}
|
|
166
224
|
if (window.PAYPAL?.apps?.Signup) {
|
|
167
|
-
|
|
225
|
+
try {
|
|
226
|
+
delete (window.PAYPAL.apps as any).Signup
|
|
227
|
+
} catch {}
|
|
168
228
|
}
|
|
169
229
|
|
|
170
230
|
const ppScript = document.createElement("script")
|
|
171
231
|
ppScript.id = "paypal-partner-js"
|
|
172
232
|
ppScript.src = scriptUrl
|
|
173
233
|
ppScript.async = true
|
|
234
|
+
ppScript.onload = () => initPartner()
|
|
174
235
|
document.body.appendChild(ppScript)
|
|
175
236
|
|
|
176
237
|
return () => {
|
|
238
|
+
cancelled = true
|
|
239
|
+
if (pollTimer) {
|
|
240
|
+
clearTimeout(pollTimer)
|
|
241
|
+
}
|
|
177
242
|
if (ppScript.parentNode) {
|
|
178
243
|
ppScript.parentNode.removeChild(ppScript)
|
|
179
244
|
}
|
|
@@ -472,15 +537,6 @@ export default function PayPalConnectionPage() {
|
|
|
472
537
|
<div>
|
|
473
538
|
<div className="text-sm text-green-600 bg-green-50 p-3 rounded border border-green-200">
|
|
474
539
|
✅ Successfully connected to PayPal!
|
|
475
|
-
<a
|
|
476
|
-
target="_blank"
|
|
477
|
-
data-paypal-button="true"
|
|
478
|
-
data-paypal-onboard-complete="onboardingCallback"
|
|
479
|
-
href="#"
|
|
480
|
-
style={{ display: "none" }}
|
|
481
|
-
>
|
|
482
|
-
PayPal
|
|
483
|
-
</a>
|
|
484
540
|
</div>
|
|
485
541
|
<div className="mt-3 rounded-md border border-ui-border-base bg-ui-bg-subtle p-3 text-xs text-ui-fg-subtle">
|
|
486
542
|
<div className="font-medium text-ui-fg-base">
|
package/src/api/middlewares.ts
CHANGED
|
@@ -2,6 +2,15 @@ import { defineMiddlewares } from "@medusajs/framework/http"
|
|
|
2
2
|
|
|
3
3
|
export default defineMiddlewares({
|
|
4
4
|
routes: [
|
|
5
|
+
{
|
|
6
|
+
// Keep the exact bytes PayPal signed (`req.rawBody`) so webhook signature
|
|
7
|
+
// verification hashes the original payload, not a re-serialized copy. This
|
|
8
|
+
// entry must precede the "/store/paypal/:path*" catch-all below.
|
|
9
|
+
matcher: "/store/paypal/webhook",
|
|
10
|
+
method: ["POST"],
|
|
11
|
+
bodyParser: { preserveRawBody: true },
|
|
12
|
+
middlewares: [],
|
|
13
|
+
},
|
|
5
14
|
{
|
|
6
15
|
matcher: "/store/paypal-complete",
|
|
7
16
|
middlewares: [],
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
-
import type {
|
|
3
|
-
import { ContainerRegistrationKeys
|
|
2
|
+
import type { Logger } from "@medusajs/framework/types"
|
|
3
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
|
4
4
|
import { randomUUID } from "crypto"
|
|
5
5
|
import type PayPalModuleService from "../../../../modules/paypal/service"
|
|
6
6
|
import { getPayPalApiBase } from "../../../../modules/paypal/utils/paypal-auth"
|
|
7
7
|
import { paypalFetch } from "../../../../modules/paypal/utils/paypal-fetch"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
findPayPalSessionForCart,
|
|
10
|
+
getStoredPayPalOrderId,
|
|
11
|
+
updatePayPalSessionData,
|
|
12
|
+
} from "../../../../modules/paypal/utils/payment-session"
|
|
9
13
|
|
|
10
14
|
const BN_CODE = "MBJTechnolabs_SI_SPB"
|
|
11
15
|
|
|
@@ -29,70 +33,6 @@ function resolveIdempotencyKey(req: MedusaRequest, suffix: string, fallback: str
|
|
|
29
33
|
return fallback || `pp-${suffix}-${randomUUID()}`
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
async function findPayPalSessionForCart(
|
|
33
|
-
cartId: string,
|
|
34
|
-
scope: any
|
|
35
|
-
): Promise<{
|
|
36
|
-
session_id: string
|
|
37
|
-
session_data: Record<string, any>
|
|
38
|
-
session_status: string
|
|
39
|
-
} | null> {
|
|
40
|
-
try {
|
|
41
|
-
const query = scope.resolve("query")
|
|
42
|
-
const { data: carts } = await query.graph({
|
|
43
|
-
entity: "cart",
|
|
44
|
-
fields: [
|
|
45
|
-
"id",
|
|
46
|
-
"payment_collection.payment_sessions.id",
|
|
47
|
-
"payment_collection.payment_sessions.data",
|
|
48
|
-
"payment_collection.payment_sessions.status",
|
|
49
|
-
"payment_collection.payment_sessions.provider_id",
|
|
50
|
-
"payment_collection.payment_sessions.created_at",
|
|
51
|
-
],
|
|
52
|
-
filters: { id: cartId },
|
|
53
|
-
})
|
|
54
|
-
const cart = carts?.[0]
|
|
55
|
-
const sessions = cart?.payment_collection?.payment_sessions || []
|
|
56
|
-
const session = sessions
|
|
57
|
-
.filter((s: any) => isPayPalProviderId(s.provider_id))
|
|
58
|
-
.sort(
|
|
59
|
-
(a: any, b: any) =>
|
|
60
|
-
new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
|
|
61
|
-
)[0]
|
|
62
|
-
|
|
63
|
-
if (!session) return null
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
session_id: session.id,
|
|
67
|
-
session_data: (session.data || {}) as Record<string, any>,
|
|
68
|
-
session_status: session.status,
|
|
69
|
-
}
|
|
70
|
-
} catch (e: any) {
|
|
71
|
-
console.warn("[PayPal] findPayPalSessionForCart failed:", e?.message)
|
|
72
|
-
return null
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function updatePayPalSessionData(
|
|
77
|
-
sessionId: string,
|
|
78
|
-
extraData: Record<string, any>,
|
|
79
|
-
scope: any
|
|
80
|
-
): Promise<void> {
|
|
81
|
-
try {
|
|
82
|
-
const paymentModule = scope.resolve(Modules.PAYMENT) as IPaymentModuleService
|
|
83
|
-
const [existing] = await paymentModule.listPaymentSessions({ id: [sessionId] }, { take: 1 })
|
|
84
|
-
const mergedData = { ...(existing?.data || {}), ...extraData }
|
|
85
|
-
await (paymentModule as any).updatePaymentSession({
|
|
86
|
-
id: sessionId,
|
|
87
|
-
data: mergedData,
|
|
88
|
-
amount: existing?.amount,
|
|
89
|
-
currency_code: existing?.currency_code,
|
|
90
|
-
})
|
|
91
|
-
} catch (e: any) {
|
|
92
|
-
console.error("[PayPal] updatePayPalSessionData failed:", e?.message)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
36
|
async function attachPayPalCaptureToSession(
|
|
97
37
|
cartId: string,
|
|
98
38
|
orderId: string,
|
|
@@ -202,10 +142,7 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
|
202
142
|
// create-order). This prevents capturing an arbitrary, caller-supplied
|
|
203
143
|
// order_id against the merchant account.
|
|
204
144
|
const session = await findPayPalSessionForCart(cartId, scope)
|
|
205
|
-
const sessionOrderId =
|
|
206
|
-
(session?.session_data?.paypal as Record<string, unknown>)?.order_id ||
|
|
207
|
-
session?.session_data?.order_id || ""
|
|
208
|
-
)
|
|
145
|
+
const sessionOrderId = getStoredPayPalOrderId(session?.session_data)
|
|
209
146
|
if (!session || !sessionOrderId) {
|
|
210
147
|
return res.status(409).json({
|
|
211
148
|
message: "No PayPal order is associated with this cart's payment session",
|