@aquarian-metals/coin-moebius-paypal 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +93 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +577 -0
- package/dist/server.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aquarian-metals
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @aquarian-metals/coin-moebius-paypal
|
|
2
|
+
|
|
3
|
+
PayPal provider for **[Coin Moebius](https://github.com/aquarian-metals/coin-moebius)**.
|
|
4
|
+
|
|
5
|
+
Two entries in one package:
|
|
6
|
+
|
|
7
|
+
- `@aquarian-metals/coin-moebius-paypal` — browser entry, redirects to PayPal's hosted approval page (Orders v2 flow).
|
|
8
|
+
- `@aquarian-metals/coin-moebius-paypal/server` — Node-only webhook verifiers (two implementations of the same contract). **Never import this from browser code.**
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
For the browser:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @aquarian-metals/coin-moebius-paypal
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
No additional dependencies required for the server verifiers — they use Web Crypto exclusively.
|
|
19
|
+
|
|
20
|
+
## Use — browser
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createPaypalProvider } from '@aquarian-metals/coin-moebius-paypal';
|
|
24
|
+
import { createPaymentManager } from '@aquarian-metals/coin-moebius';
|
|
25
|
+
|
|
26
|
+
const payments = createPaymentManager({
|
|
27
|
+
providers: [
|
|
28
|
+
createPaypalProvider({
|
|
29
|
+
sessionEndpoint: '/api/checkout/paypal',
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The session endpoint on your server is expected to call PayPal's `POST /v2/checkout/orders` with `intent: 'CAPTURE'` and return `{ url }` containing the response's `payer-action` HATEOAS link. The provider redirects the buyer to that URL and fires `onPending` synchronously. After the buyer approves and returns, your server captures the order via `POST /v2/checkout/orders/{id}/capture`; the final `PAYMENT.CAPTURE.COMPLETED` webhook lands on the server side.
|
|
36
|
+
|
|
37
|
+
## Use — server (webhook verification)
|
|
38
|
+
|
|
39
|
+
Two verifier implementations of the same `WebhookVerifier` contract. Pick one based on your traffic profile.
|
|
40
|
+
|
|
41
|
+
### Default: REST-endpoint verifier
|
|
42
|
+
|
|
43
|
+
PayPal does the crypto on their side. One OAuth call (cached for the returned `expires_in`, typically 9 hours) plus one verify call per webhook.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createPaypalVerifier } from '@aquarian-metals/coin-moebius-paypal/server';
|
|
47
|
+
|
|
48
|
+
const verify = createPaypalVerifier({
|
|
49
|
+
clientId: process.env.PAYPAL_CLIENT_ID,
|
|
50
|
+
clientSecret: process.env.PAYPAL_CLIENT_SECRET,
|
|
51
|
+
webhookId: process.env.PAYPAL_WEBHOOK_ID,
|
|
52
|
+
mode: 'live', // or 'sandbox'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// inside your webhook route:
|
|
56
|
+
const result = await verify.verify(rawBody, request.headers);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Alternative: manual verifier
|
|
60
|
+
|
|
61
|
+
Verifies the signature locally with no per-webhook PayPal round-trip after the first cert fetch. Use this when webhook volume is high enough that the extra round-trip cost matters.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createPaypalManualVerifier } from '@aquarian-metals/coin-moebius-paypal/server';
|
|
65
|
+
|
|
66
|
+
const verify = createPaypalManualVerifier({
|
|
67
|
+
webhookId: process.env.PAYPAL_WEBHOOK_ID,
|
|
68
|
+
mode: 'live',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = await verify.verify(rawBody, request.headers);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Safe by default.** Before fetching anything, the manual verifier rejects any `paypal-cert-url` whose origin is not the mode-appropriate PayPal host (`https://api.paypal.com/v1/notifications/certs/...` for live, `https://api.sandbox.paypal.com/v1/notifications/certs/...` for sandbox). HTTPS to that pinned host is the trust anchor; TLS handles cert chain validation. A forged signature header pointing at an attacker-controlled cert is refused before any network call.
|
|
75
|
+
|
|
76
|
+
### Status mapping
|
|
77
|
+
|
|
78
|
+
| PayPal event | `PaymentResult.status` |
|
|
79
|
+
| --------------------------- | ---------------------------------------------------------------- |
|
|
80
|
+
| `CHECKOUT.ORDER.APPROVED` | `pending` |
|
|
81
|
+
| `PAYMENT.CAPTURE.COMPLETED` | `success` |
|
|
82
|
+
| `PAYMENT.CAPTURE.DENIED` | `failed` |
|
|
83
|
+
| `PAYMENT.CAPTURE.DECLINED` | `failed` |
|
|
84
|
+
| `PAYMENT.CAPTURE.REFUNDED` | `refunded` |
|
|
85
|
+
| `PAYMENT.CAPTURE.REVERSED` | `refunded` (see `metadata` for the reversal indicator) |
|
|
86
|
+
| `CUSTOMER.DISPUTE.CREATED` | `disputed` |
|
|
87
|
+
| `CUSTOMER.DISPUTE.RESOLVED` | (verifier returns `null`; outcome shows on the original capture) |
|
|
88
|
+
| anything else | (verifier returns `null`, signature still validated) |
|
|
89
|
+
|
|
90
|
+
The `paymentId` field on the returned `PaymentResult` is the PayPal order id (`resource.supplementary_data.related_ids.order_id`) when available, so capture, refund, and dispute events linked to the same order all surface the same id.
|
|
91
|
+
|
|
92
|
+
### Sandbox setup
|
|
93
|
+
|
|
94
|
+
PayPal's sandbox requires two test accounts: one **business** (the merchant) and one **personal** (the buyer). Both are created from [developer.paypal.com](https://developer.paypal.com/dashboard/) under Apps & Credentials → Sandbox → Accounts.
|
|
95
|
+
|
|
96
|
+
When testing manually, set `mode: 'sandbox'` on both the verifier and your order-creation call.
|
|
97
|
+
|
|
98
|
+
### Currency support
|
|
99
|
+
|
|
100
|
+
PayPal supports a fixed list of currencies that varies by account country. USD, EUR, GBP, CAD, and AUD are universally supported. See PayPal's currency reference for the full list.
|
|
101
|
+
|
|
102
|
+
### Subscriptions
|
|
103
|
+
|
|
104
|
+
This package handles one-off PayPal orders only. PayPal's billing-agreements / subscriptions API is out of scope.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PayPal client-side provider for Coin Moebius.
|
|
3
|
+
*
|
|
4
|
+
* Hosted-checkout flow (PayPal Orders v2). The provider POSTs the buyer's
|
|
5
|
+
* selection to the caller's `sessionEndpoint`, receives `{ url }` pointing at
|
|
6
|
+
* PayPal's hosted approval page (`https://www.paypal.com/checkoutnow?token=<id>`),
|
|
7
|
+
* fires `onPending`, and redirects the buyer. PayPal handles the wallet,
|
|
8
|
+
* funding source picker, and buyer approval; the buyer then returns to the
|
|
9
|
+
* merchant's return URL where the merchant captures the order. The final
|
|
10
|
+
* `PAYMENT.CAPTURE.COMPLETED` webhook lands on the server side.
|
|
11
|
+
*
|
|
12
|
+
* import { createPaypalProvider } from '@aquarian-metals/coin-moebius-paypal';
|
|
13
|
+
* const paypal = createPaypalProvider({
|
|
14
|
+
* sessionEndpoint: '/api/checkout/paypal',
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const manager = createPaymentManager({ providers: [paypal] });
|
|
18
|
+
* await manager.initiate({ productId: 'pro', amount: 9.99, currency: 'USD' });
|
|
19
|
+
*
|
|
20
|
+
* The session endpoint is expected to call `POST /v2/checkout/orders` with
|
|
21
|
+
* `intent: 'CAPTURE'` and return `{ url }` containing the response's
|
|
22
|
+
* `payer-action` HATEOAS link.
|
|
23
|
+
*/
|
|
24
|
+
import type { PaymentProvider } from '@aquarian-metals/coin-moebius-core';
|
|
25
|
+
export interface PaypalProviderConfig {
|
|
26
|
+
/** Full URL of the session endpoint that returns `{ url: payer-action }`. */
|
|
27
|
+
sessionEndpoint: string;
|
|
28
|
+
/** Optional fetch override — used by tests. Defaults to global `fetch`. */
|
|
29
|
+
fetcher?: typeof fetch;
|
|
30
|
+
/** Optional navigation override — used by tests. Defaults to `location.assign`. */
|
|
31
|
+
navigate?: (url: string) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a `PaymentProvider` registered as `id: 'paypal'`. Fires `onPending`
|
|
35
|
+
* immediately after redirect; the actual settlement lands on the server via
|
|
36
|
+
* the webhook (`PAYMENT.CAPTURE.COMPLETED` is the canonical success event,
|
|
37
|
+
* after the merchant calls `POST /v2/checkout/orders/{id}/capture` on the
|
|
38
|
+
* buyer's return).
|
|
39
|
+
*/
|
|
40
|
+
export declare function createPaypalProvider(config: PaypalProviderConfig): PaymentProvider;
|
|
41
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EACX,eAAe,EAGf,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,oBAAoB;IACpC,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAQD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,eAAe,CAiDlF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a `PaymentProvider` registered as `id: 'paypal'`. Fires `onPending`
|
|
3
|
+
* immediately after redirect; the actual settlement lands on the server via
|
|
4
|
+
* the webhook (`PAYMENT.CAPTURE.COMPLETED` is the canonical success event,
|
|
5
|
+
* after the merchant calls `POST /v2/checkout/orders/{id}/capture` on the
|
|
6
|
+
* buyer's return).
|
|
7
|
+
*/
|
|
8
|
+
export function createPaypalProvider(config) {
|
|
9
|
+
const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
10
|
+
const navigate = config.navigate ??
|
|
11
|
+
((url) => {
|
|
12
|
+
window.location.assign(url);
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
id: 'paypal',
|
|
16
|
+
name: 'PayPal',
|
|
17
|
+
async initiate(options, callbacks) {
|
|
18
|
+
try {
|
|
19
|
+
const body = {
|
|
20
|
+
productId: options.productId,
|
|
21
|
+
amount: options.amount,
|
|
22
|
+
currency: options.currency,
|
|
23
|
+
};
|
|
24
|
+
if (options.metadata)
|
|
25
|
+
body.metadata = options.metadata;
|
|
26
|
+
const response = await fetcher(config.sessionEndpoint, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`coin-moebius/paypal: session endpoint responded ${response.status}`);
|
|
33
|
+
}
|
|
34
|
+
const payload = (await response.json());
|
|
35
|
+
if (!payload.url) {
|
|
36
|
+
throw new Error('coin-moebius/paypal: session response missing `url`');
|
|
37
|
+
}
|
|
38
|
+
const result = {
|
|
39
|
+
status: 'pending',
|
|
40
|
+
paymentId: payload.paymentId ?? '',
|
|
41
|
+
provider: 'paypal',
|
|
42
|
+
amount: options.amount,
|
|
43
|
+
currency: options.currency,
|
|
44
|
+
metadata: options.metadata ?? {},
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
callbacks.onPending?.(result);
|
|
48
|
+
navigate(payload.url);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
callbacks.onError(err instanceof Error ? err : new Error(String(err)));
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4CA;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAA4B;IAChE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACpE,MAAM,QAAQ,GACb,MAAM,CAAC,QAAQ;QACf,CAAC,CAAC,GAAW,EAAE,EAAE;YAChB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;IAEJ,OAAO;QACN,EAAE,EAAE,QAAQ;QACZ,IAAI,EAAE,QAAQ;QACd,KAAK,CAAC,QAAQ,CAAC,OAAwB,EAAE,SAAS;YACjD,IAAI,CAAC;gBACJ,MAAM,IAAI,GAA4B;oBACrC,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;iBAC1B,CAAC;gBACF,IAAI,OAAO,CAAC,QAAQ;oBAAE,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;gBAEvD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE;oBACtD,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;oBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;iBAC1B,CAAC,CAAC;gBACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,mDAAmD,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;gBACvF,CAAC;gBACD,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAoB,CAAC;gBAC3D,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;gBACxE,CAAC;gBAED,MAAM,MAAM,GAAkB;oBAC7B,MAAM,EAAE,SAAS;oBACjB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,EAAE;oBAClC,QAAQ,EAAE,QAAQ;oBAClB,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE;oBAChC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;iBACrB,CAAC;gBACF,SAAS,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAC;gBAC9B,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACd,SAAS,CAAC,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACxE,CAAC;QACF,CAAC;KACD,CAAC;AACH,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PayPal server-side webhook verifiers.
|
|
3
|
+
*
|
|
4
|
+
* Two implementations of the same `WebhookVerifier` contract — pick one:
|
|
5
|
+
*
|
|
6
|
+
* `createPaypalVerifier({ clientId, clientSecret, webhookId, mode })`
|
|
7
|
+
* POSTs the incoming transmission fields to PayPal's
|
|
8
|
+
* `/v1/notifications/verify-webhook-signature` endpoint. PayPal does the
|
|
9
|
+
* crypto; we do one OAuth call (cached for the returned `expires_in`)
|
|
10
|
+
* plus one verify call per webhook. Default choice for most callers.
|
|
11
|
+
*
|
|
12
|
+
* `createPaypalManualVerifier({ webhookId, mode, certCache?, fetcher? })`
|
|
13
|
+
* Verifies the signature locally. Faster on hot paths (no per-webhook
|
|
14
|
+
* OAuth or verify round-trip after the first cert fetch). Safe by default:
|
|
15
|
+
* rejects any `paypal-cert-url` whose origin is not the mode-appropriate
|
|
16
|
+
* PayPal host before any fetch happens, so a forged header cannot trick
|
|
17
|
+
* the verifier into trusting a third-party cert. HTTPS to the pinned
|
|
18
|
+
* host is the trust anchor; chain validation is delegated to TLS itself.
|
|
19
|
+
*
|
|
20
|
+
* Both verifiers return a `PaymentResult` for recognized events or `null`
|
|
21
|
+
* for signed-but-non-payment events. Status mapping:
|
|
22
|
+
*
|
|
23
|
+
* CHECKOUT.ORDER.APPROVED → pending
|
|
24
|
+
* PAYMENT.CAPTURE.COMPLETED → success
|
|
25
|
+
* PAYMENT.CAPTURE.DENIED → failed
|
|
26
|
+
* PAYMENT.CAPTURE.DECLINED → failed
|
|
27
|
+
* PAYMENT.CAPTURE.REFUNDED → refunded
|
|
28
|
+
* PAYMENT.CAPTURE.REVERSED → refunded
|
|
29
|
+
* CUSTOMER.DISPUTE.CREATED → disputed
|
|
30
|
+
* CUSTOMER.DISPUTE.RESOLVED → null (no status change; outcome
|
|
31
|
+
* shows on the original capture)
|
|
32
|
+
* anything else → null (signature still validated)
|
|
33
|
+
*/
|
|
34
|
+
import type { WebhookEvent } from '@aquarian-metals/coin-moebius-core';
|
|
35
|
+
export type PaypalMode = 'live' | 'sandbox';
|
|
36
|
+
export interface WebhookVerifier {
|
|
37
|
+
verify(rawBody: unknown, headers: Record<string, string | undefined>): Promise<WebhookEvent | null>;
|
|
38
|
+
}
|
|
39
|
+
/** Pluggable OAuth-token cache for the REST-endpoint verifier. */
|
|
40
|
+
export interface OAuthTokenCache {
|
|
41
|
+
get(key: string): Promise<CachedOAuthToken | null> | CachedOAuthToken | null;
|
|
42
|
+
set(key: string, value: CachedOAuthToken): Promise<void> | void;
|
|
43
|
+
}
|
|
44
|
+
export interface CachedOAuthToken {
|
|
45
|
+
accessToken: string;
|
|
46
|
+
expiresAt: number;
|
|
47
|
+
}
|
|
48
|
+
/** Pluggable cert cache for the manual verifier (cache by cert URL). */
|
|
49
|
+
export interface CertCache {
|
|
50
|
+
get(url: string): Promise<string | null> | string | null;
|
|
51
|
+
set(url: string, pem: string): Promise<void> | void;
|
|
52
|
+
}
|
|
53
|
+
export interface PaypalVerifierConfig {
|
|
54
|
+
clientId: string;
|
|
55
|
+
clientSecret: string;
|
|
56
|
+
webhookId: string;
|
|
57
|
+
mode?: PaypalMode;
|
|
58
|
+
/** Defaults to in-memory `Map` per verifier instance. */
|
|
59
|
+
tokenCache?: OAuthTokenCache;
|
|
60
|
+
/** Defaults to global `fetch`. Useful for tests. */
|
|
61
|
+
fetcher?: typeof fetch;
|
|
62
|
+
}
|
|
63
|
+
export declare function createPaypalVerifier(config: PaypalVerifierConfig): WebhookVerifier;
|
|
64
|
+
export interface PaypalManualVerifierConfig {
|
|
65
|
+
webhookId: string;
|
|
66
|
+
mode?: PaypalMode;
|
|
67
|
+
/** Defaults to in-memory `Map` keyed by cert URL. */
|
|
68
|
+
certCache?: CertCache;
|
|
69
|
+
/** Defaults to global `fetch`. Useful for tests. */
|
|
70
|
+
fetcher?: typeof fetch;
|
|
71
|
+
}
|
|
72
|
+
export declare function createPaypalManualVerifier(config: PaypalManualVerifierConfig): WebhookVerifier;
|
|
73
|
+
/**
|
|
74
|
+
* IEEE 802.3 CRC-32 of the byte array. Returns an unsigned 32-bit integer.
|
|
75
|
+
* PayPal's manual webhook verification scheme requires this as a decimal
|
|
76
|
+
* string component of the signed payload.
|
|
77
|
+
*/
|
|
78
|
+
export declare function crc32(bytes: Uint8Array): number;
|
|
79
|
+
/**
|
|
80
|
+
* Return the PayPal-hosted page a buyer manages their subscriptions on.
|
|
81
|
+
* PayPal does not offer a per-subscription "Customer Portal" session like
|
|
82
|
+
* Stripe; the buyer signs into their PayPal account and sees every
|
|
83
|
+
* automatic-payment they have, across all merchants, at the same URL. We
|
|
84
|
+
* just return that URL.
|
|
85
|
+
*
|
|
86
|
+
* The `mode` argument toggles between live and sandbox hosts so
|
|
87
|
+
* sandbox-mode test buyers don't get redirected to the live PayPal
|
|
88
|
+
* account UI (where their sandbox credentials wouldn't work).
|
|
89
|
+
*/
|
|
90
|
+
export declare function getPaypalPortalUrl(opts?: {
|
|
91
|
+
mode?: PaypalMode;
|
|
92
|
+
}): string;
|
|
93
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAItF,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC/B,MAAM,CACL,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACzC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAChC;AAED,kEAAkE;AAClE,MAAM,WAAW,eAAe;IAC/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAC7E,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAChE;AAED,MAAM,WAAW,gBAAgB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,wEAAwE;AACxE,MAAM,WAAW,SAAS;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;IACzD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACpD;AAgBD,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,yDAAyD;IACzD,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,oDAAoD;IACpD,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACvB;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,eAAe,CAwDlF;AAqDD,MAAM,WAAW,0BAA0B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,qDAAqD;IACrD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,oDAAoD;IACpD,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACvB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,0BAA0B,GAAG,eAAe,CA6C9F;AAigBD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAM/C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,UAAU,CAAA;CAAO,GAAG,MAAM,CAK3E"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PayPal server-side webhook verifiers.
|
|
3
|
+
*
|
|
4
|
+
* Two implementations of the same `WebhookVerifier` contract — pick one:
|
|
5
|
+
*
|
|
6
|
+
* `createPaypalVerifier({ clientId, clientSecret, webhookId, mode })`
|
|
7
|
+
* POSTs the incoming transmission fields to PayPal's
|
|
8
|
+
* `/v1/notifications/verify-webhook-signature` endpoint. PayPal does the
|
|
9
|
+
* crypto; we do one OAuth call (cached for the returned `expires_in`)
|
|
10
|
+
* plus one verify call per webhook. Default choice for most callers.
|
|
11
|
+
*
|
|
12
|
+
* `createPaypalManualVerifier({ webhookId, mode, certCache?, fetcher? })`
|
|
13
|
+
* Verifies the signature locally. Faster on hot paths (no per-webhook
|
|
14
|
+
* OAuth or verify round-trip after the first cert fetch). Safe by default:
|
|
15
|
+
* rejects any `paypal-cert-url` whose origin is not the mode-appropriate
|
|
16
|
+
* PayPal host before any fetch happens, so a forged header cannot trick
|
|
17
|
+
* the verifier into trusting a third-party cert. HTTPS to the pinned
|
|
18
|
+
* host is the trust anchor; chain validation is delegated to TLS itself.
|
|
19
|
+
*
|
|
20
|
+
* Both verifiers return a `PaymentResult` for recognized events or `null`
|
|
21
|
+
* for signed-but-non-payment events. Status mapping:
|
|
22
|
+
*
|
|
23
|
+
* CHECKOUT.ORDER.APPROVED → pending
|
|
24
|
+
* PAYMENT.CAPTURE.COMPLETED → success
|
|
25
|
+
* PAYMENT.CAPTURE.DENIED → failed
|
|
26
|
+
* PAYMENT.CAPTURE.DECLINED → failed
|
|
27
|
+
* PAYMENT.CAPTURE.REFUNDED → refunded
|
|
28
|
+
* PAYMENT.CAPTURE.REVERSED → refunded
|
|
29
|
+
* CUSTOMER.DISPUTE.CREATED → disputed
|
|
30
|
+
* CUSTOMER.DISPUTE.RESOLVED → null (no status change; outcome
|
|
31
|
+
* shows on the original capture)
|
|
32
|
+
* anything else → null (signature still validated)
|
|
33
|
+
*/
|
|
34
|
+
// --- mode-bound endpoints --------------------------------------------------
|
|
35
|
+
const API_BASE = {
|
|
36
|
+
live: 'https://api-m.paypal.com',
|
|
37
|
+
sandbox: 'https://api-m.sandbox.paypal.com',
|
|
38
|
+
};
|
|
39
|
+
const TRUSTED_CERT_PREFIXES = {
|
|
40
|
+
live: 'https://api.paypal.com/v1/notifications/certs/',
|
|
41
|
+
sandbox: 'https://api.sandbox.paypal.com/v1/notifications/certs/',
|
|
42
|
+
};
|
|
43
|
+
export function createPaypalVerifier(config) {
|
|
44
|
+
const mode = config.mode ?? 'live';
|
|
45
|
+
const apiBase = API_BASE[mode];
|
|
46
|
+
const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
47
|
+
const cache = config.tokenCache ?? memoryTokenCache();
|
|
48
|
+
return {
|
|
49
|
+
async verify(rawBody, headers) {
|
|
50
|
+
requireString(config.clientId, 'clientId');
|
|
51
|
+
requireString(config.clientSecret, 'clientSecret');
|
|
52
|
+
requireString(config.webhookId, 'webhookId');
|
|
53
|
+
const transmissionFields = extractTransmissionHeaders(headers);
|
|
54
|
+
const bodyString = normalizeBody(rawBody);
|
|
55
|
+
const webhookEvent = parseJson(bodyString, 'webhook event body');
|
|
56
|
+
const token = await getAccessToken({
|
|
57
|
+
cacheKey: config.clientId,
|
|
58
|
+
cache,
|
|
59
|
+
clientId: config.clientId,
|
|
60
|
+
clientSecret: config.clientSecret,
|
|
61
|
+
apiBase,
|
|
62
|
+
fetcher,
|
|
63
|
+
});
|
|
64
|
+
const verifyResponse = await fetcher(`${apiBase}/v1/notifications/verify-webhook-signature`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${token}`,
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
auth_algo: transmissionFields.authAlgo,
|
|
72
|
+
cert_url: transmissionFields.certUrl,
|
|
73
|
+
transmission_id: transmissionFields.transmissionId,
|
|
74
|
+
transmission_sig: transmissionFields.transmissionSig,
|
|
75
|
+
transmission_time: transmissionFields.transmissionTime,
|
|
76
|
+
webhook_id: config.webhookId,
|
|
77
|
+
webhook_event: webhookEvent,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
if (!verifyResponse.ok) {
|
|
81
|
+
const text = await verifyResponse.text();
|
|
82
|
+
throw new Error(`coin-moebius/paypal: verify-webhook-signature failed (${verifyResponse.status}): ${text}`);
|
|
83
|
+
}
|
|
84
|
+
const verifyPayload = (await verifyResponse.json());
|
|
85
|
+
if (verifyPayload.verification_status !== 'SUCCESS') {
|
|
86
|
+
throw new Error('coin-moebius/paypal: invalid signature');
|
|
87
|
+
}
|
|
88
|
+
return toPaymentResult(webhookEvent);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function getAccessToken(input) {
|
|
93
|
+
const cached = await input.cache.get(input.cacheKey);
|
|
94
|
+
// Refresh when within 60s of expiry to avoid a near-miss race.
|
|
95
|
+
if (cached && cached.expiresAt - Date.now() > 60_000) {
|
|
96
|
+
return cached.accessToken;
|
|
97
|
+
}
|
|
98
|
+
const basic = base64(`${input.clientId}:${input.clientSecret}`);
|
|
99
|
+
const response = await input.fetcher(`${input.apiBase}/v1/oauth2/token`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Basic ${basic}`,
|
|
103
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
104
|
+
Accept: 'application/json',
|
|
105
|
+
},
|
|
106
|
+
body: 'grant_type=client_credentials',
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const text = await response.text();
|
|
110
|
+
throw new Error(`coin-moebius/paypal: oauth token request failed (${response.status}): ${text}`);
|
|
111
|
+
}
|
|
112
|
+
const payload = (await response.json());
|
|
113
|
+
if (!payload.access_token || typeof payload.expires_in !== 'number') {
|
|
114
|
+
throw new Error('coin-moebius/paypal: oauth response missing access_token or expires_in');
|
|
115
|
+
}
|
|
116
|
+
const expiresAt = Date.now() + payload.expires_in * 1000;
|
|
117
|
+
await input.cache.set(input.cacheKey, { accessToken: payload.access_token, expiresAt });
|
|
118
|
+
return payload.access_token;
|
|
119
|
+
}
|
|
120
|
+
function memoryTokenCache() {
|
|
121
|
+
const store = new Map();
|
|
122
|
+
return {
|
|
123
|
+
get: (key) => store.get(key) ?? null,
|
|
124
|
+
set: (key, value) => {
|
|
125
|
+
store.set(key, value);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function createPaypalManualVerifier(config) {
|
|
130
|
+
const mode = config.mode ?? 'live';
|
|
131
|
+
const trustedPrefix = TRUSTED_CERT_PREFIXES[mode];
|
|
132
|
+
const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
133
|
+
const cache = config.certCache ?? memoryCertCache();
|
|
134
|
+
return {
|
|
135
|
+
async verify(rawBody, headers) {
|
|
136
|
+
requireString(config.webhookId, 'webhookId');
|
|
137
|
+
const transmissionFields = extractTransmissionHeaders(headers);
|
|
138
|
+
// SAFE-BY-DEFAULT GUARD: refuse to fetch any cert URL that is not
|
|
139
|
+
// on the mode-appropriate PayPal host. HTTPS to a pinned host is
|
|
140
|
+
// the trust anchor; TLS handles cert chain validation for us.
|
|
141
|
+
if (!transmissionFields.certUrl.startsWith(trustedPrefix)) {
|
|
142
|
+
throw new Error(`coin-moebius/paypal: refusing untrusted paypal-cert-url (must start with ${trustedPrefix})`);
|
|
143
|
+
}
|
|
144
|
+
const bodyBytes = bodyToBytes(rawBody);
|
|
145
|
+
const bodyString = new TextDecoder().decode(bodyBytes);
|
|
146
|
+
const webhookEvent = parseJson(bodyString, 'webhook event body');
|
|
147
|
+
const crc = crc32(bodyBytes).toString(10);
|
|
148
|
+
const signedString = `${transmissionFields.transmissionId}|${transmissionFields.transmissionTime}|${config.webhookId}|${crc}`;
|
|
149
|
+
const pem = await fetchCertPem(transmissionFields.certUrl, cache, fetcher);
|
|
150
|
+
const publicKey = await importRsaPublicKeyFromCert(pem);
|
|
151
|
+
const signatureBytes = base64Decode(transmissionFields.transmissionSig);
|
|
152
|
+
const verified = await crypto.subtle.verify({ name: 'RSASSA-PKCS1-v1_5' }, publicKey, signatureBytes, new TextEncoder().encode(signedString));
|
|
153
|
+
if (!verified) {
|
|
154
|
+
throw new Error('coin-moebius/paypal: invalid signature');
|
|
155
|
+
}
|
|
156
|
+
return toPaymentResult(webhookEvent);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function fetchCertPem(url, cache, fetcher) {
|
|
161
|
+
const cached = await cache.get(url);
|
|
162
|
+
if (cached)
|
|
163
|
+
return cached;
|
|
164
|
+
const response = await fetcher(url, { method: 'GET' });
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error(`coin-moebius/paypal: cert fetch failed (${response.status}) for ${url}`);
|
|
167
|
+
}
|
|
168
|
+
const pem = await response.text();
|
|
169
|
+
await cache.set(url, pem);
|
|
170
|
+
return pem;
|
|
171
|
+
}
|
|
172
|
+
function memoryCertCache() {
|
|
173
|
+
const store = new Map();
|
|
174
|
+
return {
|
|
175
|
+
get: (url) => store.get(url) ?? null,
|
|
176
|
+
set: (url, pem) => {
|
|
177
|
+
store.set(url, pem);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Extract the SubjectPublicKeyInfo from an X.509 certificate (PEM) and import
|
|
183
|
+
* it as an RSA public key for SHA-256 verification.
|
|
184
|
+
*
|
|
185
|
+
* Web Crypto's `importKey('spki', ...)` accepts SubjectPublicKeyInfo bytes,
|
|
186
|
+
* not full X.509 certificates. We walk the cert's ASN.1 structure to extract
|
|
187
|
+
* the SPKI bytes, then hand them to Web Crypto. The walk is minimal: enter
|
|
188
|
+
* the outer Certificate SEQUENCE, enter the tbsCertificate SEQUENCE, skip
|
|
189
|
+
* the explicit-version, serial, signature, issuer, validity, subject fields,
|
|
190
|
+
* then read the SubjectPublicKeyInfo SEQUENCE as a complete TLV slice.
|
|
191
|
+
*/
|
|
192
|
+
async function importRsaPublicKeyFromCert(pem) {
|
|
193
|
+
const der = pemBlockToBytes(pem, 'CERTIFICATE');
|
|
194
|
+
const spki = extractSpkiFromX509(der);
|
|
195
|
+
return crypto.subtle.importKey('spki', spki, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']);
|
|
196
|
+
}
|
|
197
|
+
function extractSpkiFromX509(der) {
|
|
198
|
+
// Outer: SEQUENCE (Certificate) — tag 0x30
|
|
199
|
+
const certSeq = readTlv(der, 0);
|
|
200
|
+
if (certSeq.tag !== 0x30) {
|
|
201
|
+
throw new Error('coin-moebius/paypal: unexpected outer ASN.1 tag (expected SEQUENCE)');
|
|
202
|
+
}
|
|
203
|
+
// Inside Certificate: tbsCertificate SEQUENCE — tag 0x30
|
|
204
|
+
const tbs = readTlv(certSeq.value, 0);
|
|
205
|
+
if (tbs.tag !== 0x30) {
|
|
206
|
+
throw new Error('coin-moebius/paypal: unexpected tbsCertificate tag (expected SEQUENCE)');
|
|
207
|
+
}
|
|
208
|
+
let offset = 0;
|
|
209
|
+
const body = tbs.value;
|
|
210
|
+
// Optional [0] EXPLICIT Version. Tag 0xA0 if present; skip when seen.
|
|
211
|
+
if (body[offset] === 0xa0) {
|
|
212
|
+
const version = readTlv(body, offset);
|
|
213
|
+
offset = version.end;
|
|
214
|
+
}
|
|
215
|
+
// CertificateSerialNumber — INTEGER (0x02)
|
|
216
|
+
offset = readTlv(body, offset).end;
|
|
217
|
+
// signature AlgorithmIdentifier — SEQUENCE (0x30)
|
|
218
|
+
offset = readTlv(body, offset).end;
|
|
219
|
+
// issuer Name — SEQUENCE (0x30)
|
|
220
|
+
offset = readTlv(body, offset).end;
|
|
221
|
+
// validity — SEQUENCE (0x30)
|
|
222
|
+
offset = readTlv(body, offset).end;
|
|
223
|
+
// subject Name — SEQUENCE (0x30)
|
|
224
|
+
offset = readTlv(body, offset).end;
|
|
225
|
+
// Next field is the SubjectPublicKeyInfo SEQUENCE. We need the entire
|
|
226
|
+
// TLV (tag + length + value) as a slice, because Web Crypto's `spki`
|
|
227
|
+
// format expects the full ASN.1 SubjectPublicKeyInfo structure.
|
|
228
|
+
const spkiTlv = readTlv(body, offset);
|
|
229
|
+
if (spkiTlv.tag !== 0x30) {
|
|
230
|
+
throw new Error('coin-moebius/paypal: expected SubjectPublicKeyInfo SEQUENCE at the post-subject position');
|
|
231
|
+
}
|
|
232
|
+
return body.subarray(offset, spkiTlv.end);
|
|
233
|
+
}
|
|
234
|
+
function readTlv(bytes, start) {
|
|
235
|
+
if (start >= bytes.length) {
|
|
236
|
+
throw new Error('coin-moebius/paypal: truncated ASN.1 input');
|
|
237
|
+
}
|
|
238
|
+
const tag = bytes[start];
|
|
239
|
+
let cursor = start + 1;
|
|
240
|
+
const lengthByte = bytes[cursor++];
|
|
241
|
+
let length;
|
|
242
|
+
if (lengthByte < 0x80) {
|
|
243
|
+
length = lengthByte;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const numLengthBytes = lengthByte & 0x7f;
|
|
247
|
+
if (numLengthBytes === 0 || numLengthBytes > 4) {
|
|
248
|
+
// Forbid indefinite-length (numLengthBytes === 0) and anything
|
|
249
|
+
// over 32-bit since we're dealing with kB-scale certs.
|
|
250
|
+
throw new Error('coin-moebius/paypal: unsupported ASN.1 length encoding');
|
|
251
|
+
}
|
|
252
|
+
length = 0;
|
|
253
|
+
for (let i = 0; i < numLengthBytes; i++) {
|
|
254
|
+
length = (length << 8) | bytes[cursor++];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const valueStart = cursor;
|
|
258
|
+
const end = valueStart + length;
|
|
259
|
+
if (end > bytes.length) {
|
|
260
|
+
throw new Error('coin-moebius/paypal: ASN.1 length exceeds input');
|
|
261
|
+
}
|
|
262
|
+
return { tag, length, value: bytes.subarray(valueStart, end), end };
|
|
263
|
+
}
|
|
264
|
+
function extractTransmissionHeaders(headers) {
|
|
265
|
+
const transmissionId = headerValue(headers, 'paypal-transmission-id');
|
|
266
|
+
const transmissionTime = headerValue(headers, 'paypal-transmission-time');
|
|
267
|
+
const certUrl = headerValue(headers, 'paypal-cert-url');
|
|
268
|
+
const transmissionSig = headerValue(headers, 'paypal-transmission-sig');
|
|
269
|
+
// `paypal-auth-algo` is required for the REST verifier; the manual
|
|
270
|
+
// verifier assumes SHA256withRSA (PayPal's documented default) and
|
|
271
|
+
// ignores it, but we still surface it for forwarding.
|
|
272
|
+
const authAlgo = headerValue(headers, 'paypal-auth-algo') ?? 'SHA256withRSA';
|
|
273
|
+
if (!transmissionId || !transmissionTime || !certUrl || !transmissionSig) {
|
|
274
|
+
throw new Error('coin-moebius/paypal: missing one or more required paypal-* headers');
|
|
275
|
+
}
|
|
276
|
+
return { transmissionId, transmissionTime, certUrl, transmissionSig, authAlgo };
|
|
277
|
+
}
|
|
278
|
+
function headerValue(headers, name) {
|
|
279
|
+
const lower = name.toLowerCase();
|
|
280
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
281
|
+
if (key.toLowerCase() === lower)
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
function normalizeBody(rawBody) {
|
|
287
|
+
if (typeof rawBody === 'string')
|
|
288
|
+
return rawBody;
|
|
289
|
+
if (rawBody instanceof Uint8Array)
|
|
290
|
+
return new TextDecoder().decode(rawBody);
|
|
291
|
+
if (rawBody && typeof rawBody === 'object')
|
|
292
|
+
return JSON.stringify(rawBody);
|
|
293
|
+
throw new Error('coin-moebius/paypal: unsupported body type');
|
|
294
|
+
}
|
|
295
|
+
function bodyToBytes(rawBody) {
|
|
296
|
+
if (rawBody instanceof Uint8Array)
|
|
297
|
+
return rawBody;
|
|
298
|
+
if (typeof rawBody === 'string')
|
|
299
|
+
return new TextEncoder().encode(rawBody);
|
|
300
|
+
if (rawBody && typeof rawBody === 'object') {
|
|
301
|
+
return new TextEncoder().encode(JSON.stringify(rawBody));
|
|
302
|
+
}
|
|
303
|
+
throw new Error('coin-moebius/paypal: unsupported body type');
|
|
304
|
+
}
|
|
305
|
+
function parseJson(s, label) {
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(s);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
throw new Error(`coin-moebius/paypal: ${label} is not valid JSON`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function requireString(value, fieldName) {
|
|
314
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
315
|
+
throw new Error(`coin-moebius/paypal: ${fieldName} missing on verifier config`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function toPaymentResult(event) {
|
|
319
|
+
const typed = event;
|
|
320
|
+
const eventType = typed.event_type ?? '';
|
|
321
|
+
// Subscription lifecycle events take precedence over the one-time event
|
|
322
|
+
// mapping. The order events (PAYMENT.SALE.COMPLETED in particular) can
|
|
323
|
+
// fire for both legacy one-time payments AND for subscription cycles —
|
|
324
|
+
// the subscription path narrows by inspecting the resource shape.
|
|
325
|
+
const subscriptionEvent = toSubscriptionEvent(event, eventType);
|
|
326
|
+
if (subscriptionEvent)
|
|
327
|
+
return subscriptionEvent;
|
|
328
|
+
const status = mapEventType(eventType);
|
|
329
|
+
if (status === null)
|
|
330
|
+
return null;
|
|
331
|
+
const resource = typed.resource ?? {};
|
|
332
|
+
const { amount, currency } = readAmountAndCurrency(resource, eventType);
|
|
333
|
+
const paymentId = readPaymentId(resource);
|
|
334
|
+
return {
|
|
335
|
+
kind: 'payment',
|
|
336
|
+
status,
|
|
337
|
+
paymentId,
|
|
338
|
+
provider: 'paypal',
|
|
339
|
+
amount,
|
|
340
|
+
currency,
|
|
341
|
+
metadata: {
|
|
342
|
+
paypalEventType: eventType,
|
|
343
|
+
paypalResourceId: typeof resource.id === 'string' ? resource.id : undefined,
|
|
344
|
+
},
|
|
345
|
+
timestamp: Date.now(),
|
|
346
|
+
raw: event,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* PayPal subscription-event mapper. Recognizes five event types covering
|
|
351
|
+
* the subscription lifecycle and emits the SDK's normalized
|
|
352
|
+
* `SubscriptionEvent`. Returns `null` for any other event so the caller
|
|
353
|
+
* falls through to the payment-event path.
|
|
354
|
+
*
|
|
355
|
+
* BILLING.SUBSCRIPTION.ACTIVATED → subscription.created
|
|
356
|
+
* PAYMENT.SALE.COMPLETED (sub-linked) → subscription.renewed
|
|
357
|
+
* BILLING.SUBSCRIPTION.PAYMENT.FAILED → subscription.payment_failed
|
|
358
|
+
* BILLING.SUBSCRIPTION.UPDATED → subscription.updated
|
|
359
|
+
* BILLING.SUBSCRIPTION.CANCELLED → subscription.canceled
|
|
360
|
+
*
|
|
361
|
+
* `PAYMENT.SALE.COMPLETED` is the only fork: PayPal uses it for legacy
|
|
362
|
+
* one-time payments AND for subscription cycle charges. We discriminate
|
|
363
|
+
* by checking whether the resource's `billing_agreement_id` is set, which
|
|
364
|
+
* it is on subscription-linked sales.
|
|
365
|
+
*/
|
|
366
|
+
function toSubscriptionEvent(event, eventType) {
|
|
367
|
+
const typed = event;
|
|
368
|
+
const resource = (typed.resource ?? {});
|
|
369
|
+
const isSaleForSubscription = eventType === 'PAYMENT.SALE.COMPLETED' && typeof resource.billing_agreement_id === 'string';
|
|
370
|
+
let subscriptionType = null;
|
|
371
|
+
switch (eventType) {
|
|
372
|
+
case 'BILLING.SUBSCRIPTION.ACTIVATED':
|
|
373
|
+
subscriptionType = 'subscription.created';
|
|
374
|
+
break;
|
|
375
|
+
case 'BILLING.SUBSCRIPTION.PAYMENT.FAILED':
|
|
376
|
+
subscriptionType = 'subscription.payment_failed';
|
|
377
|
+
break;
|
|
378
|
+
case 'BILLING.SUBSCRIPTION.UPDATED':
|
|
379
|
+
case 'BILLING.SUBSCRIPTION.SUSPENDED':
|
|
380
|
+
case 'BILLING.SUBSCRIPTION.RE-ACTIVATED':
|
|
381
|
+
case 'BILLING.SUBSCRIPTION.EXPIRED':
|
|
382
|
+
subscriptionType = 'subscription.updated';
|
|
383
|
+
break;
|
|
384
|
+
case 'BILLING.SUBSCRIPTION.CANCELLED':
|
|
385
|
+
subscriptionType = 'subscription.canceled';
|
|
386
|
+
break;
|
|
387
|
+
default:
|
|
388
|
+
if (isSaleForSubscription) {
|
|
389
|
+
subscriptionType = 'subscription.renewed';
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const subscriptionId = isSaleForSubscription
|
|
396
|
+
? (resource.billing_agreement_id ?? '')
|
|
397
|
+
: (resource.id ?? '');
|
|
398
|
+
if (!subscriptionId)
|
|
399
|
+
return null;
|
|
400
|
+
const status = mapSubscriptionStatus(eventType, resource.status);
|
|
401
|
+
const { amount, currency } = readSubscriptionAmount(resource, isSaleForSubscription);
|
|
402
|
+
const nextBilling = resource.billing_info?.next_billing_time;
|
|
403
|
+
const currentPeriodEnd = typeof nextBilling === 'string' ? Math.floor(new Date(nextBilling).getTime() / 1000) : null;
|
|
404
|
+
return {
|
|
405
|
+
kind: 'subscription',
|
|
406
|
+
type: subscriptionType,
|
|
407
|
+
subscriptionId,
|
|
408
|
+
provider: 'paypal',
|
|
409
|
+
productId: typeof resource.plan_id === 'string' ? resource.plan_id : null,
|
|
410
|
+
customerRef: typeof resource.subscriber?.payer_id === 'string' ? resource.subscriber.payer_id : null,
|
|
411
|
+
status,
|
|
412
|
+
currentPeriodEnd: Number.isFinite(currentPeriodEnd) ? currentPeriodEnd : null,
|
|
413
|
+
amount,
|
|
414
|
+
currency,
|
|
415
|
+
metadata: {
|
|
416
|
+
paypalEventType: eventType,
|
|
417
|
+
...(typeof resource.custom_id === 'string' ? { customerRef: resource.custom_id } : {}),
|
|
418
|
+
},
|
|
419
|
+
timestamp: Date.now(),
|
|
420
|
+
raw: event,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Map PayPal's subscription status field onto our neutral enum. PayPal
|
|
425
|
+
* statuses: `APPROVAL_PENDING`, `APPROVED`, `ACTIVE`, `SUSPENDED`,
|
|
426
|
+
* `CANCELLED`, `EXPIRED`. The event type itself sometimes carries more
|
|
427
|
+
* intent than the status field (e.g. CANCELLED sub still shows `ACTIVE`
|
|
428
|
+
* status briefly), so we let the event type win where they disagree.
|
|
429
|
+
*/
|
|
430
|
+
function mapSubscriptionStatus(eventType, rawStatus) {
|
|
431
|
+
if (eventType === 'BILLING.SUBSCRIPTION.CANCELLED')
|
|
432
|
+
return 'canceled';
|
|
433
|
+
if (eventType === 'BILLING.SUBSCRIPTION.PAYMENT.FAILED')
|
|
434
|
+
return 'past_due';
|
|
435
|
+
if (eventType === 'BILLING.SUBSCRIPTION.SUSPENDED')
|
|
436
|
+
return 'paused';
|
|
437
|
+
switch (rawStatus) {
|
|
438
|
+
case 'ACTIVE':
|
|
439
|
+
case 'APPROVED':
|
|
440
|
+
return 'active';
|
|
441
|
+
case 'SUSPENDED':
|
|
442
|
+
return 'paused';
|
|
443
|
+
case 'CANCELLED':
|
|
444
|
+
case 'EXPIRED':
|
|
445
|
+
return 'canceled';
|
|
446
|
+
default:
|
|
447
|
+
return 'unknown';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function readSubscriptionAmount(resource, isSaleForSubscription) {
|
|
451
|
+
const amountField = isSaleForSubscription
|
|
452
|
+
? resource.amount
|
|
453
|
+
: resource.billing_info?.last_payment?.amount;
|
|
454
|
+
const value = amountField?.value;
|
|
455
|
+
const amount = typeof value === 'string' ? Number.parseFloat(value) : 0;
|
|
456
|
+
const currency = (amountField?.currency_code ?? 'USD').toUpperCase();
|
|
457
|
+
return { amount: Number.isFinite(amount) ? amount : 0, currency };
|
|
458
|
+
}
|
|
459
|
+
function mapEventType(eventType) {
|
|
460
|
+
switch (eventType) {
|
|
461
|
+
case 'CHECKOUT.ORDER.APPROVED':
|
|
462
|
+
return 'pending';
|
|
463
|
+
case 'PAYMENT.CAPTURE.COMPLETED':
|
|
464
|
+
return 'success';
|
|
465
|
+
case 'PAYMENT.CAPTURE.DENIED':
|
|
466
|
+
case 'PAYMENT.CAPTURE.DECLINED':
|
|
467
|
+
return 'failed';
|
|
468
|
+
case 'PAYMENT.CAPTURE.REFUNDED':
|
|
469
|
+
case 'PAYMENT.CAPTURE.REVERSED':
|
|
470
|
+
return 'refunded';
|
|
471
|
+
case 'CUSTOMER.DISPUTE.CREATED':
|
|
472
|
+
return 'disputed';
|
|
473
|
+
// CUSTOMER.DISPUTE.RESOLVED intentionally returns null: the resolution
|
|
474
|
+
// outcome is reflected on the original capture event's downstream
|
|
475
|
+
// state, not as a separate status change.
|
|
476
|
+
default:
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function readAmountAndCurrency(resource, eventType) {
|
|
481
|
+
// Disputes use `dispute_amount`; captures use `amount`; orders nest amount
|
|
482
|
+
// inside the first `purchase_units` entry. Probe each, default safely.
|
|
483
|
+
let amountField;
|
|
484
|
+
if (eventType === 'CUSTOMER.DISPUTE.CREATED') {
|
|
485
|
+
amountField = resource.dispute_amount;
|
|
486
|
+
}
|
|
487
|
+
else if (resource.amount) {
|
|
488
|
+
amountField = resource.amount;
|
|
489
|
+
}
|
|
490
|
+
else if (resource.purchase_units?.[0]?.amount) {
|
|
491
|
+
amountField = resource.purchase_units[0].amount;
|
|
492
|
+
}
|
|
493
|
+
const value = amountField?.value;
|
|
494
|
+
const amount = typeof value === 'string' ? Number.parseFloat(value) : 0;
|
|
495
|
+
const currency = (amountField?.currency_code ?? 'USD').toUpperCase();
|
|
496
|
+
return { amount: Number.isFinite(amount) ? amount : 0, currency };
|
|
497
|
+
}
|
|
498
|
+
function readPaymentId(resource) {
|
|
499
|
+
// Prefer the underlying order id when a capture/refund event references
|
|
500
|
+
// one — gives consumers a stable id across the full payment lifecycle.
|
|
501
|
+
// Fall back to the resource id (the capture or order id itself).
|
|
502
|
+
const orderId = resource.supplementary_data?.related_ids?.order_id;
|
|
503
|
+
if (typeof orderId === 'string' && orderId.length > 0)
|
|
504
|
+
return orderId;
|
|
505
|
+
if (typeof resource.id === 'string')
|
|
506
|
+
return resource.id;
|
|
507
|
+
return '';
|
|
508
|
+
}
|
|
509
|
+
// --- crypto + encoding helpers --------------------------------------------
|
|
510
|
+
function pemBlockToBytes(pem, label) {
|
|
511
|
+
const begin = `-----BEGIN ${label}-----`;
|
|
512
|
+
const end = `-----END ${label}-----`;
|
|
513
|
+
const beginIdx = pem.indexOf(begin);
|
|
514
|
+
const endIdx = pem.indexOf(end);
|
|
515
|
+
if (beginIdx === -1 || endIdx === -1) {
|
|
516
|
+
throw new Error(`coin-moebius/paypal: PEM missing ${label} block`);
|
|
517
|
+
}
|
|
518
|
+
const body = pem.slice(beginIdx + begin.length, endIdx).replace(/\s+/g, '');
|
|
519
|
+
return base64Decode(body);
|
|
520
|
+
}
|
|
521
|
+
function base64(input) {
|
|
522
|
+
let binary = '';
|
|
523
|
+
const bytes = new TextEncoder().encode(input);
|
|
524
|
+
for (const b of bytes)
|
|
525
|
+
binary += String.fromCharCode(b);
|
|
526
|
+
return btoa(binary);
|
|
527
|
+
}
|
|
528
|
+
function base64Decode(input) {
|
|
529
|
+
const binary = atob(input);
|
|
530
|
+
const bytes = new Uint8Array(binary.length);
|
|
531
|
+
for (let i = 0; i < binary.length; i++)
|
|
532
|
+
bytes[i] = binary.charCodeAt(i);
|
|
533
|
+
return bytes;
|
|
534
|
+
}
|
|
535
|
+
// --- CRC32 -----------------------------------------------------------------
|
|
536
|
+
const CRC32_TABLE = buildCrc32Table();
|
|
537
|
+
function buildCrc32Table() {
|
|
538
|
+
const table = new Uint32Array(256);
|
|
539
|
+
for (let i = 0; i < 256; i++) {
|
|
540
|
+
let c = i;
|
|
541
|
+
for (let k = 0; k < 8; k++) {
|
|
542
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
543
|
+
}
|
|
544
|
+
table[i] = c >>> 0;
|
|
545
|
+
}
|
|
546
|
+
return table;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* IEEE 802.3 CRC-32 of the byte array. Returns an unsigned 32-bit integer.
|
|
550
|
+
* PayPal's manual webhook verification scheme requires this as a decimal
|
|
551
|
+
* string component of the signed payload.
|
|
552
|
+
*/
|
|
553
|
+
export function crc32(bytes) {
|
|
554
|
+
let crc = 0xffffffff;
|
|
555
|
+
for (const byte of bytes) {
|
|
556
|
+
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ byte) & 0xff];
|
|
557
|
+
}
|
|
558
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Return the PayPal-hosted page a buyer manages their subscriptions on.
|
|
562
|
+
* PayPal does not offer a per-subscription "Customer Portal" session like
|
|
563
|
+
* Stripe; the buyer signs into their PayPal account and sees every
|
|
564
|
+
* automatic-payment they have, across all merchants, at the same URL. We
|
|
565
|
+
* just return that URL.
|
|
566
|
+
*
|
|
567
|
+
* The `mode` argument toggles between live and sandbox hosts so
|
|
568
|
+
* sandbox-mode test buyers don't get redirected to the live PayPal
|
|
569
|
+
* account UI (where their sandbox credentials wouldn't work).
|
|
570
|
+
*/
|
|
571
|
+
export function getPaypalPortalUrl(opts = {}) {
|
|
572
|
+
const mode = opts.mode ?? 'live';
|
|
573
|
+
return mode === 'sandbox'
|
|
574
|
+
? 'https://www.sandbox.paypal.com/myaccount/autopay/'
|
|
575
|
+
: 'https://www.paypal.com/myaccount/autopay/';
|
|
576
|
+
}
|
|
577
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aquarian-metals/coin-moebius-paypal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PayPal provider for Coin Moebius — Orders v2 redirect flow, plus two server-only webhook verifiers (REST endpoint or local RSA verification with cert URL pinning).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"payments",
|
|
7
|
+
"paypal",
|
|
8
|
+
"orders-v2",
|
|
9
|
+
"checkout",
|
|
10
|
+
"webhook",
|
|
11
|
+
"coin-moebius"
|
|
12
|
+
],
|
|
13
|
+
"author": "aquarian-metals",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://github.com/aquarian-metals/coin-moebius#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/aquarian-metals/coin-moebius/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/aquarian-metals/coin-moebius.git",
|
|
22
|
+
"directory": "packages/providers/paypal"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./server": {
|
|
37
|
+
"types": "./dist/server.d.ts",
|
|
38
|
+
"import": "./dist/server.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
44
|
+
"build": "npm run clean && tsc",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@aquarian-metals/coin-moebius-core": "^1.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"typescript": "~5.8.0"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|