@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.
Files changed (53) hide show
  1. package/.medusa/server/src/admin/index.js +59 -32
  2. package/.medusa/server/src/admin/index.mjs +59 -32
  3. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts +4 -1
  4. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.d.ts.map +1 -1
  5. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js +64 -2
  6. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js.map +1 -1
  7. package/.medusa/server/src/api/middlewares.d.ts.map +1 -1
  8. package/.medusa/server/src/api/middlewares.js +9 -0
  9. package/.medusa/server/src/api/middlewares.js.map +1 -1
  10. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
  11. package/.medusa/server/src/api/store/paypal/capture-order/route.js +8 -58
  12. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  13. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  14. package/.medusa/server/src/api/store/paypal/create-order/route.js +17 -43
  15. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  16. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
  17. package/.medusa/server/src/api/store/paypal/webhook/route.js +13 -8
  18. package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
  19. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
  20. package/.medusa/server/src/modules/paypal/payment-provider/service.js +54 -31
  21. package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
  22. package/.medusa/server/src/modules/paypal/payment-provider/status-utils.d.ts +32 -0
  23. package/.medusa/server/src/modules/paypal/payment-provider/status-utils.d.ts.map +1 -0
  24. package/.medusa/server/src/modules/paypal/payment-provider/status-utils.js +65 -0
  25. package/.medusa/server/src/modules/paypal/payment-provider/status-utils.js.map +1 -0
  26. package/.medusa/server/src/modules/paypal/service.d.ts +12 -1
  27. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  28. package/.medusa/server/src/modules/paypal/service.js +14 -3
  29. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  30. package/.medusa/server/src/modules/paypal/utils/payment-session.d.ts +35 -0
  31. package/.medusa/server/src/modules/paypal/utils/payment-session.d.ts.map +1 -0
  32. package/.medusa/server/src/modules/paypal/utils/payment-session.js +78 -0
  33. package/.medusa/server/src/modules/paypal/utils/payment-session.js.map +1 -0
  34. package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts +2 -1
  35. package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts.map +1 -1
  36. package/.medusa/server/src/modules/paypal/utils/secret-crypto.js +82 -26
  37. package/.medusa/server/src/modules/paypal/utils/secret-crypto.js.map +1 -1
  38. package/.medusa/server/src/modules/paypal/utils/webhook-verify.d.ts +28 -0
  39. package/.medusa/server/src/modules/paypal/utils/webhook-verify.d.ts.map +1 -0
  40. package/.medusa/server/src/modules/paypal/utils/webhook-verify.js +58 -0
  41. package/.medusa/server/src/modules/paypal/utils/webhook-verify.js.map +1 -0
  42. package/package.json +1 -1
  43. package/src/admin/routes/settings/paypal/connection/page.tsx +68 -12
  44. package/src/api/middlewares.ts +9 -0
  45. package/src/api/store/paypal/capture-order/route.ts +8 -71
  46. package/src/api/store/paypal/create-order/route.ts +25 -47
  47. package/src/api/store/paypal/webhook/route.ts +22 -8
  48. package/src/modules/paypal/payment-provider/service.ts +73 -29
  49. package/src/modules/paypal/payment-provider/status-utils.ts +59 -0
  50. package/src/modules/paypal/service.ts +17 -3
  51. package/src/modules/paypal/utils/payment-session.ts +104 -0
  52. package/src/modules/paypal/utils/secret-crypto.ts +90 -27
  53. 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":"AAuCA,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEnD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CAavF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CAsBvF"}
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:v1:<iv_b64>:<tag_b64>:<ciphertext_b64>` using AES-256-GCM
17
- * (authenticated, so tampering is detected on decrypt). Legacy plaintext values
18
- * (no prefix) are returned as-is on decrypt, so secrets stored before
19
- * encryption was enabled keep working and are upgraded on the next save.
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 PREFIX = "enc:v1:";
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 cachedKey = null;
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
- function getKey() {
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
- // Accept a key of any length/format; derive a stable 32-byte AES key.
37
- cachedKey = raw ? (0, crypto_1.createHash)("sha256").update(raw).digest() : null;
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 cachedKey;
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 getKey() !== null;
87
+ return getRawKey() !== null;
43
88
  }
44
89
  function isEncrypted(value) {
45
- return typeof value === "string" && value.startsWith(PREFIX);
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 key = getKey();
56
- if (!key || isEncrypted(value)) {
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 `${PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ciphertext.toString("base64")}`;
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 key = getKey();
75
- if (!key) {
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
- const body = String(value).slice(PREFIX.length);
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
- const decipher = (0, crypto_1.createDecipheriv)("aes-256-gcm", bytes(key), bytes(Buffer.from(ivB64, "base64")));
84
- decipher.setAuthTag(bytes(Buffer.from(tagB64, "base64")));
85
- const plaintext = Buffer.concat([
86
- bytes(decipher.update(bytes(Buffer.from(ctB64, "base64")))),
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":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easypayment/medusa-payment-paypal",
3
- "version": "0.9.16",
3
+ "version": "0.9.18",
4
4
  "description": "PayPal integration for Medusa v2",
5
5
  "author": "EasyPayment Plugins",
6
6
  "license": "MIT",
@@ -24,8 +24,9 @@ declare global {
24
24
  PAYPAL?: {
25
25
  apps?: {
26
26
  Signup?: {
27
- miniBrowser?: { init: () => void }
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
- delete (window.PAYPAL.apps as any).Signup
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">
@@ -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 { IPaymentModuleService, Logger } from "@medusajs/framework/types"
3
- import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
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 { isPayPalProviderId } from "../../../../modules/paypal/utils/provider-ids"
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 = String(
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",