@aquarian-metals/coin-moebius-coinbase-business 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 +116 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +83 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +203 -0
- package/dist/server.js.map +1 -0
- package/dist/subscription.d.ts +75 -0
- package/dist/subscription.d.ts.map +1 -0
- package/dist/subscription.js +140 -0
- package/dist/subscription.js.map +1 -0
- package/package.json +65 -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,116 @@
|
|
|
1
|
+
# @aquarian-metals/coin-moebius-coinbase-business
|
|
2
|
+
|
|
3
|
+
Coinbase Business provider for **[Coin Moebius](https://github.com/aquarian-metals/coin-moebius)**.
|
|
4
|
+
|
|
5
|
+
Three entries in one package:
|
|
6
|
+
|
|
7
|
+
- `@aquarian-metals/coin-moebius-coinbase-business` — browser entry, redirects to Coinbase's hosted checkout.
|
|
8
|
+
- `@aquarian-metals/coin-moebius-coinbase-business/server` — Node-only webhook verifier (Hook0 v1 signatures). **Never import this from browser code.**
|
|
9
|
+
- `@aquarian-metals/coin-moebius-coinbase-business/subscription` — optional, server-only helper that creates a webhook subscription via the CDP API. Import this if you want to provision the subscription from your own code; ignore it if you manage the subscription out-of-band.
|
|
10
|
+
|
|
11
|
+
## Replaces Coinbase Commerce
|
|
12
|
+
|
|
13
|
+
🚨 Coinbase Commerce was sunset for new merchants and the migration cutover ran on March 31, 2026. The legacy `coinbase-commerce-node` SDK targets the deprecated Commerce surface. This package targets the current Coinbase Business Checkout API instead.
|
|
14
|
+
|
|
15
|
+
## Geography
|
|
16
|
+
|
|
17
|
+
Coinbase Business currently supports merchants registered in the United States or Singapore only. Merchants in other jurisdictions cannot use this provider until Coinbase expands eligibility.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
For the browser:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @aquarian-metals/coin-moebius-coinbase-business
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No additional dependencies required for the server verifier — it uses Web Crypto exclusively.
|
|
28
|
+
|
|
29
|
+
## Use — browser
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createCoinbaseBusinessProvider } from '@aquarian-metals/coin-moebius-coinbase-business';
|
|
33
|
+
import { createPaymentManager } from '@aquarian-metals/coin-moebius';
|
|
34
|
+
|
|
35
|
+
const payments = createPaymentManager({
|
|
36
|
+
providers: [
|
|
37
|
+
createCoinbaseBusinessProvider({
|
|
38
|
+
sessionEndpoint: '/api/checkout/coinbase-business',
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The session endpoint on your server is expected to call Coinbase's Checkout API and return `{ url: hosted_url }`. The provider redirects the buyer to `hosted_url` and fires `onPending` synchronously.
|
|
45
|
+
|
|
46
|
+
## Use — server (webhook verification)
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createCoinbaseBusinessVerifier } from '@aquarian-metals/coin-moebius-coinbase-business/server';
|
|
50
|
+
|
|
51
|
+
const verify = createCoinbaseBusinessVerifier({
|
|
52
|
+
webhookSecret: process.env.COINBASE_BUSINESS_WEBHOOK_SECRET,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// inside your webhook route:
|
|
56
|
+
const result = await verify.verify(rawBody, request.headers);
|
|
57
|
+
if (result) {
|
|
58
|
+
// result.status is one of: 'success' | 'failed'
|
|
59
|
+
// result.paymentId is the Coinbase checkout id
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Status mapping
|
|
64
|
+
|
|
65
|
+
| Coinbase event | `PaymentResult.status` |
|
|
66
|
+
| -------------------------- | ---------------------------------------------------- |
|
|
67
|
+
| `checkout.payment.success` | `success` |
|
|
68
|
+
| `checkout.payment.failed` | `failed` |
|
|
69
|
+
| `checkout.payment.expired` | `failed` |
|
|
70
|
+
| anything else | (verifier returns `null`, signature still validated) |
|
|
71
|
+
|
|
72
|
+
Coinbase Business does not emit an in-flight `pending` event. The buyer experience between session creation and the first webhook is "awaiting payment" by absence-of-event, not a positive signal. If your UX needs an interim state, render it from the moment your session endpoint responds and clear it on the webhook.
|
|
73
|
+
|
|
74
|
+
### Webhook signature format
|
|
75
|
+
|
|
76
|
+
Coinbase routes Business webhooks through [Hook0](https://documentation.hook0.com/). The signature header is structured, not a bare digest:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
X-Hook0-Signature: t=<unix-seconds>,h=<space-separated-header-names>,v1=<hex-sha256>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The signed content is `${t}.${h}.${headerValues.join('.')}.${rawBody}`, HMAC-SHA256 with the webhook secret. The verifier handles the parsing for you, but if you ever need to verify by hand the format is the same as Hook0's published Node.js reference.
|
|
83
|
+
|
|
84
|
+
The verifier also enforces a 5-minute replay window by default. Override with `maxAgeSeconds` if you have a legitimate reason (e.g., replaying captured fixtures).
|
|
85
|
+
|
|
86
|
+
## Use — server (programmatic webhook subscription)
|
|
87
|
+
|
|
88
|
+
Coinbase Business does not expose a dashboard form to add a webhook URL. The subscription must be created via API, and the signing secret is **only returned on the create response** — you cannot retrieve it later.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createCoinbaseBusinessSubscription } from '@aquarian-metals/coin-moebius-coinbase-business/subscription';
|
|
92
|
+
|
|
93
|
+
const sub = createCoinbaseBusinessSubscription({
|
|
94
|
+
cdpKeyId: process.env.CDP_KEY_ID,
|
|
95
|
+
cdpPrivateKeyPem: process.env.CDP_PRIVATE_KEY_PEM,
|
|
96
|
+
mode: 'live',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const { subscriptionId, signingSecret } = await sub.subscribe({
|
|
100
|
+
callbackUrl: 'https://app.example.com/webhook/coinbase-business',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Persist signingSecret now. It is not retrievable later.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### CDP key setup
|
|
107
|
+
|
|
108
|
+
1. Create a CDP account at [portal.cdp.coinbase.com](https://portal.cdp.coinbase.com).
|
|
109
|
+
2. Under API Keys, create a new key. Coinbase issues an EC P-256 keypair; download the private key (PEM, PKCS8) when prompted.
|
|
110
|
+
3. Pass the key id to `cdpKeyId` and the PEM contents to `cdpPrivateKeyPem`.
|
|
111
|
+
|
|
112
|
+
The helper signs requests with ES256 (ECDSA P-256 + SHA-256). Coinbase also supports Ed25519, but ES256 has broader Web Crypto support across older runtimes and Coinbase accepts both.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coinbase Business client-side provider for Coin Moebius.
|
|
3
|
+
*
|
|
4
|
+
* Hosted-checkout flow only. The provider POSTs the buyer's selection to the
|
|
5
|
+
* caller's `sessionEndpoint`, receives `{ url }` pointing at Coinbase
|
|
6
|
+
* Business's `hosted_url`, fires `onPending`, and redirects the buyer there.
|
|
7
|
+
* Coinbase handles the asset picker, on-chain monitoring, and forwarding.
|
|
8
|
+
*
|
|
9
|
+
* The server-side webhook verifier lives at `./server` and the optional
|
|
10
|
+
* programmatic subscription helper at `./subscription`, so this browser
|
|
11
|
+
* entry stays free of Node-only crypto and HTTP code.
|
|
12
|
+
*
|
|
13
|
+
* import { createCoinbaseBusinessProvider } from '@aquarian-metals/coin-moebius-coinbase-business';
|
|
14
|
+
* const coinbase = createCoinbaseBusinessProvider({
|
|
15
|
+
* sessionEndpoint: '/api/checkout/coinbase-business',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const manager = createPaymentManager({ providers: [coinbase] });
|
|
19
|
+
* await manager.initiate({ productId: 'pro', amount: 9.99, currency: 'USD' });
|
|
20
|
+
*/
|
|
21
|
+
import type { PaymentProvider } from '@aquarian-metals/coin-moebius-core';
|
|
22
|
+
export interface CoinbaseBusinessProviderConfig {
|
|
23
|
+
/** Full URL of the session endpoint that returns `{ url: hosted_url }`. */
|
|
24
|
+
sessionEndpoint: string;
|
|
25
|
+
/** Optional fetch override — used by tests. Defaults to global `fetch`. */
|
|
26
|
+
fetcher?: typeof fetch;
|
|
27
|
+
/** Optional navigation override — used by tests. Defaults to `location.assign`. */
|
|
28
|
+
navigate?: (url: string) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build a `PaymentProvider` registered as `id: 'coinbase-business'`. Returns a
|
|
32
|
+
* `PaymentResult` with status `'pending'` immediately after redirect; the
|
|
33
|
+
* actual settlement lands on the server via the Hook0-signed webhook.
|
|
34
|
+
* Coinbase Business does not fire any in-flight event before `success` /
|
|
35
|
+
* `failed` / `expired`, so consumers wanting buyer-side completion should
|
|
36
|
+
* also call `manager.subscribeToStatus(paymentId, …)` after `initiate`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCoinbaseBusinessProvider(config: CoinbaseBusinessProviderConfig): PaymentProvider;
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EACX,eAAe,EAGf,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,8BAA8B;IAC9C,2EAA2E;IAC3E,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;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAC7C,MAAM,EAAE,8BAA8B,GACpC,eAAe,CAmDjB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a `PaymentProvider` registered as `id: 'coinbase-business'`. Returns a
|
|
3
|
+
* `PaymentResult` with status `'pending'` immediately after redirect; the
|
|
4
|
+
* actual settlement lands on the server via the Hook0-signed webhook.
|
|
5
|
+
* Coinbase Business does not fire any in-flight event before `success` /
|
|
6
|
+
* `failed` / `expired`, so consumers wanting buyer-side completion should
|
|
7
|
+
* also call `manager.subscribeToStatus(paymentId, …)` after `initiate`.
|
|
8
|
+
*/
|
|
9
|
+
export function createCoinbaseBusinessProvider(config) {
|
|
10
|
+
const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
11
|
+
const navigate = config.navigate ??
|
|
12
|
+
((url) => {
|
|
13
|
+
window.location.assign(url);
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
id: 'coinbase-business',
|
|
17
|
+
name: 'Coinbase Business',
|
|
18
|
+
async initiate(options, callbacks) {
|
|
19
|
+
try {
|
|
20
|
+
const body = {
|
|
21
|
+
productId: options.productId,
|
|
22
|
+
amount: options.amount,
|
|
23
|
+
currency: options.currency,
|
|
24
|
+
};
|
|
25
|
+
if (options.metadata)
|
|
26
|
+
body.metadata = options.metadata;
|
|
27
|
+
const response = await fetcher(config.sessionEndpoint, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new Error(`coin-moebius/coinbase-business: session endpoint responded ${response.status}`);
|
|
34
|
+
}
|
|
35
|
+
const payload = (await response.json());
|
|
36
|
+
if (!payload.url) {
|
|
37
|
+
throw new Error('coin-moebius/coinbase-business: session response missing `url`');
|
|
38
|
+
}
|
|
39
|
+
const result = {
|
|
40
|
+
status: 'pending',
|
|
41
|
+
paymentId: payload.paymentId ?? '',
|
|
42
|
+
provider: 'coinbase-business',
|
|
43
|
+
amount: options.amount,
|
|
44
|
+
currency: options.currency,
|
|
45
|
+
metadata: options.metadata ?? {},
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
};
|
|
48
|
+
callbacks.onPending?.(result);
|
|
49
|
+
navigate(payload.url);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
callbacks.onError(err instanceof Error ? err : new Error(String(err)));
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAyCA;;;;;;;GAOG;AACH,MAAM,UAAU,8BAA8B,CAC7C,MAAsC;IAEtC,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,mBAAmB;QACvB,IAAI,EAAE,mBAAmB;QACzB,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,CACd,8DAA8D,QAAQ,CAAC,MAAM,EAAE,CAC/E,CAAC;gBACH,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,gEAAgE,CAAC,CAAC;gBACnF,CAAC;gBAED,MAAM,MAAM,GAAkB;oBAC7B,MAAM,EAAE,SAAS;oBACjB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,EAAE;oBAClC,QAAQ,EAAE,mBAAmB;oBAC7B,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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coinbase Business server-side webhook verifier. Implements the Hook0 v1
|
|
3
|
+
* signature scheme (Coinbase routes Business Checkout webhooks through Hook0;
|
|
4
|
+
* see <https://documentation.hook0.com/tutorials/webhook-authentication>):
|
|
5
|
+
*
|
|
6
|
+
* X-Hook0-Signature: t=<unix-seconds>,h=<space-separated-header-names>,v1=<hex-sha256>
|
|
7
|
+
*
|
|
8
|
+
* signed = `${t}.${h}.${headerValues.join('.')}.${rawBody}`
|
|
9
|
+
* v1 = HMAC-SHA256(signed, webhookSecret) // hex-encoded
|
|
10
|
+
*
|
|
11
|
+
* Where `headerValues` are the inbound request's values for the headers
|
|
12
|
+
* listed (in order) in the `h=` field. The `h=` field is space-separated and
|
|
13
|
+
* header names are matched case-insensitively. If `h=` is absent (Hook0 v0
|
|
14
|
+
* legacy mode), the signed content is `${t}.${rawBody}`.
|
|
15
|
+
*
|
|
16
|
+
* A replay-window guard (default 300 seconds) rejects messages where
|
|
17
|
+
* `now - t > maxAgeSeconds`. Tests pass `Number.POSITIVE_INFINITY` to bypass
|
|
18
|
+
* wall-clock drift on captured fixtures.
|
|
19
|
+
*
|
|
20
|
+
* The verifier returns a `PaymentResult` for the three Checkout API event
|
|
21
|
+
* types and resolves to `null` for any other signed event so consumers can
|
|
22
|
+
* skip non-payment deliveries without polluting their transaction store:
|
|
23
|
+
*
|
|
24
|
+
* checkout.payment.success → success
|
|
25
|
+
* checkout.payment.failed → failed
|
|
26
|
+
* checkout.payment.expired → failed (treated as terminal negative)
|
|
27
|
+
*
|
|
28
|
+
* Coinbase Business does not emit an in-flight `pending` event; absence-of-
|
|
29
|
+
* event is the "awaiting payment" signal between checkout creation and the
|
|
30
|
+
* first webhook landing.
|
|
31
|
+
*/
|
|
32
|
+
import type { WebhookEvent } from '@aquarian-metals/coin-moebius-core';
|
|
33
|
+
export interface CoinbaseBusinessVerifierConfig {
|
|
34
|
+
/**
|
|
35
|
+
* Webhook signing secret returned by Coinbase at subscription creation
|
|
36
|
+
* time. Coinbase does not expose this secret in a dashboard after
|
|
37
|
+
* creation — capture it on the subscription response and persist it.
|
|
38
|
+
*/
|
|
39
|
+
webhookSecret: string;
|
|
40
|
+
/**
|
|
41
|
+
* Maximum age, in seconds, that a webhook's `t=` timestamp can be before
|
|
42
|
+
* the verifier rejects it. Defaults to 300 (matches Hook0's default
|
|
43
|
+
* replay window). Set to `Number.POSITIVE_INFINITY` in tests against
|
|
44
|
+
* fixed-time fixtures.
|
|
45
|
+
*/
|
|
46
|
+
maxAgeSeconds?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Clock override. Returns the current time in seconds. Defaults to
|
|
49
|
+
* `Math.floor(Date.now() / 1000)`. Useful for deterministic tests.
|
|
50
|
+
*/
|
|
51
|
+
now?: () => number;
|
|
52
|
+
}
|
|
53
|
+
export interface WebhookVerifier {
|
|
54
|
+
verify(rawBody: unknown, headers: Record<string, string | undefined>): Promise<WebhookEvent | null>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parsed components of the `X-Hook0-Signature` header. Exposed for callers
|
|
58
|
+
* that want to validate the header shape without running the full HMAC step.
|
|
59
|
+
*/
|
|
60
|
+
export interface ParsedHook0Signature {
|
|
61
|
+
timestamp: number;
|
|
62
|
+
headerNames: string[];
|
|
63
|
+
signature: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function createCoinbaseBusinessVerifier(config: CoinbaseBusinessVerifierConfig): WebhookVerifier;
|
|
66
|
+
/**
|
|
67
|
+
* Parse a raw `X-Hook0-Signature` header value into its `t`, `h`, `v1`
|
|
68
|
+
* components. Throws if any required field is missing or malformed.
|
|
69
|
+
*
|
|
70
|
+
* Exported so callers with non-standard rawBody pipelines can validate the
|
|
71
|
+
* header shape without going through the full verifier.
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseHook0Signature(header: string): ParsedHook0Signature;
|
|
74
|
+
/**
|
|
75
|
+
* Compute the canonical Hook0 v1 signature. Hex-encoded HMAC-SHA256 over
|
|
76
|
+
* `${t}.${h}.${headerValues.join('.')}.${rawBody}`, or `${t}.${rawBody}` when
|
|
77
|
+
* `headerNames` is empty (v0 legacy form).
|
|
78
|
+
*
|
|
79
|
+
* Exported so callers can verify with the same routine without re-importing
|
|
80
|
+
* internals.
|
|
81
|
+
*/
|
|
82
|
+
export declare function computeCoinbaseBusinessSignature(timestamp: number, headerNames: readonly string[], headerValues: readonly string[], rawBody: string, webhookSecret: string): Promise<string>;
|
|
83
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAEtF,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,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;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CAClB;AAID,wBAAgB,8BAA8B,CAC7C,MAAM,EAAE,8BAA8B,GACpC,eAAe,CA0CjB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,CAuBxE;AAED;;;;;;;GAOG;AACH,wBAAsB,gCAAgC,CACrD,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coinbase Business server-side webhook verifier. Implements the Hook0 v1
|
|
3
|
+
* signature scheme (Coinbase routes Business Checkout webhooks through Hook0;
|
|
4
|
+
* see <https://documentation.hook0.com/tutorials/webhook-authentication>):
|
|
5
|
+
*
|
|
6
|
+
* X-Hook0-Signature: t=<unix-seconds>,h=<space-separated-header-names>,v1=<hex-sha256>
|
|
7
|
+
*
|
|
8
|
+
* signed = `${t}.${h}.${headerValues.join('.')}.${rawBody}`
|
|
9
|
+
* v1 = HMAC-SHA256(signed, webhookSecret) // hex-encoded
|
|
10
|
+
*
|
|
11
|
+
* Where `headerValues` are the inbound request's values for the headers
|
|
12
|
+
* listed (in order) in the `h=` field. The `h=` field is space-separated and
|
|
13
|
+
* header names are matched case-insensitively. If `h=` is absent (Hook0 v0
|
|
14
|
+
* legacy mode), the signed content is `${t}.${rawBody}`.
|
|
15
|
+
*
|
|
16
|
+
* A replay-window guard (default 300 seconds) rejects messages where
|
|
17
|
+
* `now - t > maxAgeSeconds`. Tests pass `Number.POSITIVE_INFINITY` to bypass
|
|
18
|
+
* wall-clock drift on captured fixtures.
|
|
19
|
+
*
|
|
20
|
+
* The verifier returns a `PaymentResult` for the three Checkout API event
|
|
21
|
+
* types and resolves to `null` for any other signed event so consumers can
|
|
22
|
+
* skip non-payment deliveries without polluting their transaction store:
|
|
23
|
+
*
|
|
24
|
+
* checkout.payment.success → success
|
|
25
|
+
* checkout.payment.failed → failed
|
|
26
|
+
* checkout.payment.expired → failed (treated as terminal negative)
|
|
27
|
+
*
|
|
28
|
+
* Coinbase Business does not emit an in-flight `pending` event; absence-of-
|
|
29
|
+
* event is the "awaiting payment" signal between checkout creation and the
|
|
30
|
+
* first webhook landing.
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_MAX_AGE_SECONDS = 300;
|
|
33
|
+
export function createCoinbaseBusinessVerifier(config) {
|
|
34
|
+
const maxAge = config.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
|
|
35
|
+
const now = config.now ?? (() => Math.floor(Date.now() / 1000));
|
|
36
|
+
return {
|
|
37
|
+
async verify(rawBody, headers) {
|
|
38
|
+
if (!config.webhookSecret) {
|
|
39
|
+
throw new Error('coin-moebius/coinbase-business: webhookSecret missing on verifier config');
|
|
40
|
+
}
|
|
41
|
+
const signatureHeader = headerValue(headers, 'x-hook0-signature');
|
|
42
|
+
if (!signatureHeader) {
|
|
43
|
+
throw new Error('coin-moebius/coinbase-business: missing x-hook0-signature header');
|
|
44
|
+
}
|
|
45
|
+
const parsed = parseHook0Signature(signatureHeader);
|
|
46
|
+
// Replay-window guard. Distinct from signature failure so callers
|
|
47
|
+
// can log the two cases separately if they want.
|
|
48
|
+
const age = Math.abs(now() - parsed.timestamp);
|
|
49
|
+
if (age > maxAge) {
|
|
50
|
+
throw new Error(`coin-moebius/coinbase-business: webhook timestamp outside replay window (age ${age}s, max ${maxAge}s)`);
|
|
51
|
+
}
|
|
52
|
+
const bodyString = normalizeBody(rawBody);
|
|
53
|
+
const expected = await computeCoinbaseBusinessSignature(parsed.timestamp, parsed.headerNames, parsed.headerNames.map((name) => headerValue(headers, name) ?? ''), bodyString, config.webhookSecret);
|
|
54
|
+
if (!timingSafeStringEqual(expected, parsed.signature)) {
|
|
55
|
+
throw new Error('coin-moebius/coinbase-business: invalid signature');
|
|
56
|
+
}
|
|
57
|
+
return toPaymentResult(bodyString);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse a raw `X-Hook0-Signature` header value into its `t`, `h`, `v1`
|
|
63
|
+
* components. Throws if any required field is missing or malformed.
|
|
64
|
+
*
|
|
65
|
+
* Exported so callers with non-standard rawBody pipelines can validate the
|
|
66
|
+
* header shape without going through the full verifier.
|
|
67
|
+
*/
|
|
68
|
+
export function parseHook0Signature(header) {
|
|
69
|
+
const parts = {};
|
|
70
|
+
for (const piece of header.split(',')) {
|
|
71
|
+
const eq = piece.indexOf('=');
|
|
72
|
+
if (eq === -1)
|
|
73
|
+
continue;
|
|
74
|
+
const key = piece.slice(0, eq).trim();
|
|
75
|
+
const value = piece.slice(eq + 1).trim();
|
|
76
|
+
if (key)
|
|
77
|
+
parts[key] = value;
|
|
78
|
+
}
|
|
79
|
+
const t = Number.parseInt(parts.t ?? '', 10);
|
|
80
|
+
if (!Number.isFinite(t) || t <= 0) {
|
|
81
|
+
throw new Error('coin-moebius/coinbase-business: signature header missing or invalid `t`');
|
|
82
|
+
}
|
|
83
|
+
const signature = parts.v1;
|
|
84
|
+
if (!signature) {
|
|
85
|
+
throw new Error('coin-moebius/coinbase-business: signature header missing `v1`');
|
|
86
|
+
}
|
|
87
|
+
// `h` is optional. Hook0 v0 deliveries omit it; the signed payload then
|
|
88
|
+
// is just `${t}.${rawBody}`. We handle both shapes via an empty list.
|
|
89
|
+
const headerNames = parts.h ? parts.h.split(/\s+/).filter(Boolean) : [];
|
|
90
|
+
return { timestamp: t, headerNames, signature };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Compute the canonical Hook0 v1 signature. Hex-encoded HMAC-SHA256 over
|
|
94
|
+
* `${t}.${h}.${headerValues.join('.')}.${rawBody}`, or `${t}.${rawBody}` when
|
|
95
|
+
* `headerNames` is empty (v0 legacy form).
|
|
96
|
+
*
|
|
97
|
+
* Exported so callers can verify with the same routine without re-importing
|
|
98
|
+
* internals.
|
|
99
|
+
*/
|
|
100
|
+
export async function computeCoinbaseBusinessSignature(timestamp, headerNames, headerValues, rawBody, webhookSecret) {
|
|
101
|
+
if (headerNames.length !== headerValues.length) {
|
|
102
|
+
throw new Error('coin-moebius/coinbase-business: headerNames / headerValues length mismatch');
|
|
103
|
+
}
|
|
104
|
+
const signed = headerNames.length === 0
|
|
105
|
+
? `${timestamp}.${rawBody}`
|
|
106
|
+
: `${timestamp}.${headerNames.join(' ')}.${headerValues.join('.')}.${rawBody}`;
|
|
107
|
+
const message = new TextEncoder().encode(signed);
|
|
108
|
+
const keyBytes = new TextEncoder().encode(webhookSecret);
|
|
109
|
+
const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
110
|
+
const sigBuf = await crypto.subtle.sign('HMAC', key, message);
|
|
111
|
+
return toHex(new Uint8Array(sigBuf));
|
|
112
|
+
}
|
|
113
|
+
function toPaymentResult(bodyString) {
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(bodyString);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new Error('coin-moebius/coinbase-business: body is not valid JSON');
|
|
120
|
+
}
|
|
121
|
+
const eventType = parsed.event?.type ?? parsed.type ?? '';
|
|
122
|
+
const data = parsed.event?.data ?? parsed.data ?? {};
|
|
123
|
+
const status = mapEventType(eventType);
|
|
124
|
+
if (status === null)
|
|
125
|
+
return null;
|
|
126
|
+
const { amount, currency } = readAmountAndCurrency(data);
|
|
127
|
+
return {
|
|
128
|
+
kind: 'payment',
|
|
129
|
+
status,
|
|
130
|
+
paymentId: String(data.checkout_id ?? data.id ?? ''),
|
|
131
|
+
provider: 'coinbase-business',
|
|
132
|
+
amount,
|
|
133
|
+
currency,
|
|
134
|
+
metadata: {
|
|
135
|
+
...(data.metadata ?? {}),
|
|
136
|
+
coinbaseEventType: eventType,
|
|
137
|
+
},
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
raw: parsed,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Map Coinbase Business event types to the SDK's `PaymentStatus` union.
|
|
144
|
+
* Unknown events return `null` so consumers can skip non-payment deliveries.
|
|
145
|
+
*
|
|
146
|
+
* checkout.payment.success → success
|
|
147
|
+
* checkout.payment.failed → failed
|
|
148
|
+
* checkout.payment.expired → failed (terminal negative, treated like fail)
|
|
149
|
+
*/
|
|
150
|
+
function mapEventType(eventType) {
|
|
151
|
+
switch (eventType) {
|
|
152
|
+
case 'checkout.payment.success':
|
|
153
|
+
return 'success';
|
|
154
|
+
case 'checkout.payment.failed':
|
|
155
|
+
case 'checkout.payment.expired':
|
|
156
|
+
return 'failed';
|
|
157
|
+
default:
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function readAmountAndCurrency(data) {
|
|
162
|
+
// Coinbase Business returns the local-currency price under a few shapes
|
|
163
|
+
// depending on the API revision. Probe each and fall back to zero/USD
|
|
164
|
+
// so the verifier never throws on an otherwise-valid signed event.
|
|
165
|
+
const local = data.pricing?.local ?? data.local_price;
|
|
166
|
+
const rawAmount = local?.amount ?? data.amount;
|
|
167
|
+
const amount = typeof rawAmount === 'string' ? Number.parseFloat(rawAmount) : (rawAmount ?? 0);
|
|
168
|
+
const currency = (local?.currency ?? data.currency ?? 'USD').toUpperCase();
|
|
169
|
+
return { amount: Number.isFinite(amount) ? amount : 0, currency };
|
|
170
|
+
}
|
|
171
|
+
function normalizeBody(rawBody) {
|
|
172
|
+
if (typeof rawBody === 'string')
|
|
173
|
+
return rawBody;
|
|
174
|
+
if (rawBody instanceof Uint8Array)
|
|
175
|
+
return new TextDecoder().decode(rawBody);
|
|
176
|
+
if (rawBody && typeof rawBody === 'object')
|
|
177
|
+
return JSON.stringify(rawBody);
|
|
178
|
+
throw new Error('coin-moebius/coinbase-business: unsupported body type');
|
|
179
|
+
}
|
|
180
|
+
function headerValue(headers, name) {
|
|
181
|
+
const lower = name.toLowerCase();
|
|
182
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
183
|
+
if (key.toLowerCase() === lower)
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
function timingSafeStringEqual(a, b) {
|
|
189
|
+
if (a.length !== b.length)
|
|
190
|
+
return false;
|
|
191
|
+
let mismatch = 0;
|
|
192
|
+
for (let i = 0; i < a.length; i++) {
|
|
193
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
194
|
+
}
|
|
195
|
+
return mismatch === 0;
|
|
196
|
+
}
|
|
197
|
+
function toHex(bytes) {
|
|
198
|
+
let out = '';
|
|
199
|
+
for (const b of bytes)
|
|
200
|
+
out += b.toString(16).padStart(2, '0');
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic webhook-subscription helper for Coinbase Business. Calls the
|
|
3
|
+
* CDP webhook subscriptions endpoint to register a callback URL, signing it
|
|
4
|
+
* with a CDP-issued JWT. The response contains the webhook signing secret;
|
|
5
|
+
* **this is the only time Coinbase exposes that secret**, so the caller must
|
|
6
|
+
* persist it before returning to the user.
|
|
7
|
+
*
|
|
8
|
+
* Coinbase Business does not expose a dashboard form to paste a webhook URL.
|
|
9
|
+
* Subscription creation is programmatic-only — callers who want to manage
|
|
10
|
+
* webhook subscriptions in their own provisioning flow import this module.
|
|
11
|
+
* Callers who only need to verify incoming webhooks can ignore it entirely.
|
|
12
|
+
*
|
|
13
|
+
* The JWT is signed with ES256 (ECDSA P-256 + SHA-256) via Web Crypto, which
|
|
14
|
+
* is universally available on Workers, Bun, Node 18+, and modern browsers.
|
|
15
|
+
* Coinbase's docs also list Ed25519/EdDSA as "recommended" but Web Crypto
|
|
16
|
+
* support for Ed25519 is uneven across older runtimes; ES256 is the safe
|
|
17
|
+
* cross-runtime choice and Coinbase accepts both.
|
|
18
|
+
*/
|
|
19
|
+
export type CoinbaseBusinessMode = 'live' | 'sandbox';
|
|
20
|
+
export interface CoinbaseBusinessSubscriptionConfig {
|
|
21
|
+
/** CDP API key id (the `kid` field on the JWT). */
|
|
22
|
+
cdpKeyId: string;
|
|
23
|
+
/**
|
|
24
|
+
* CDP-issued EC P-256 private key in PEM form (PKCS8). Coinbase shows
|
|
25
|
+
* this once at key creation; persist it on the caller side.
|
|
26
|
+
*/
|
|
27
|
+
cdpPrivateKeyPem: string;
|
|
28
|
+
/** Selects sandbox vs production CDP endpoints. */
|
|
29
|
+
mode?: CoinbaseBusinessMode;
|
|
30
|
+
/** Optional fetch override — used by tests. Defaults to global `fetch`. */
|
|
31
|
+
fetcher?: typeof fetch;
|
|
32
|
+
}
|
|
33
|
+
export interface SubscribeOptions {
|
|
34
|
+
/** Public HTTPS URL where Coinbase will deliver webhook events. */
|
|
35
|
+
callbackUrl: string;
|
|
36
|
+
/**
|
|
37
|
+
* Event types to subscribe to. Defaults to the three Checkout payment
|
|
38
|
+
* events the verifier maps. Pass an empty array to receive every
|
|
39
|
+
* event Coinbase emits (not recommended; the verifier returns `null`
|
|
40
|
+
* for non-payment events anyway, but bandwidth is bandwidth).
|
|
41
|
+
*/
|
|
42
|
+
eventTypes?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
export interface SubscribeResult {
|
|
45
|
+
/** Coinbase subscription id; persist for future deletes/updates. */
|
|
46
|
+
subscriptionId: string;
|
|
47
|
+
/**
|
|
48
|
+
* The webhook signing secret. **Not retrievable later** — persist this
|
|
49
|
+
* now or you'll need to delete and recreate the subscription.
|
|
50
|
+
*/
|
|
51
|
+
signingSecret: string;
|
|
52
|
+
/** Raw response payload for callers that need additional fields. */
|
|
53
|
+
raw: unknown;
|
|
54
|
+
}
|
|
55
|
+
export declare function createCoinbaseBusinessSubscription(config: CoinbaseBusinessSubscriptionConfig): {
|
|
56
|
+
subscribe(options: SubscribeOptions): Promise<SubscribeResult>;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Build and sign a CDP-flavored JWT. Exposed so callers integrating other
|
|
60
|
+
* CDP endpoints (Advanced Trade, Onchain) can reuse the same routine.
|
|
61
|
+
*
|
|
62
|
+
* The JWT pattern is documented at
|
|
63
|
+
* <https://docs.cdp.coinbase.com/get-started/authentication/jwt-authentication>:
|
|
64
|
+
*
|
|
65
|
+
* header = { alg: 'ES256', typ: 'JWT', kid: <keyId>, nonce: <random> }
|
|
66
|
+
* payload = { sub: <keyId>, iss: 'cdp', aud: ['cdp_service'],
|
|
67
|
+
* nbf: <now>, exp: <now + 120>, uri: '<METHOD> <host+path>' }
|
|
68
|
+
*/
|
|
69
|
+
export declare function signCdpJwt(options: {
|
|
70
|
+
keyId: string;
|
|
71
|
+
privateKeyPem: string;
|
|
72
|
+
method: string;
|
|
73
|
+
url: string;
|
|
74
|
+
}): Promise<string>;
|
|
75
|
+
//# sourceMappingURL=subscription.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,kCAAkC;IAClD,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,IAAI,CAAC,EAAE,oBAAoB,CAAC;IAC5B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,GAAG,EAAE,OAAO,CAAC;CACb;AAUD,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,kCAAkC;uBAIlE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;EAwCrE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,MAAM,CAAC,CAkClB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic webhook-subscription helper for Coinbase Business. Calls the
|
|
3
|
+
* CDP webhook subscriptions endpoint to register a callback URL, signing it
|
|
4
|
+
* with a CDP-issued JWT. The response contains the webhook signing secret;
|
|
5
|
+
* **this is the only time Coinbase exposes that secret**, so the caller must
|
|
6
|
+
* persist it before returning to the user.
|
|
7
|
+
*
|
|
8
|
+
* Coinbase Business does not expose a dashboard form to paste a webhook URL.
|
|
9
|
+
* Subscription creation is programmatic-only — callers who want to manage
|
|
10
|
+
* webhook subscriptions in their own provisioning flow import this module.
|
|
11
|
+
* Callers who only need to verify incoming webhooks can ignore it entirely.
|
|
12
|
+
*
|
|
13
|
+
* The JWT is signed with ES256 (ECDSA P-256 + SHA-256) via Web Crypto, which
|
|
14
|
+
* is universally available on Workers, Bun, Node 18+, and modern browsers.
|
|
15
|
+
* Coinbase's docs also list Ed25519/EdDSA as "recommended" but Web Crypto
|
|
16
|
+
* support for Ed25519 is uneven across older runtimes; ES256 is the safe
|
|
17
|
+
* cross-runtime choice and Coinbase accepts both.
|
|
18
|
+
*/
|
|
19
|
+
const SUBSCRIPTION_URL = 'https://api.cdp.coinbase.com/platform/v2/data/webhooks/subscriptions';
|
|
20
|
+
const JWT_LIFETIME_SECONDS = 120;
|
|
21
|
+
const DEFAULT_EVENT_TYPES = [
|
|
22
|
+
'checkout.payment.success',
|
|
23
|
+
'checkout.payment.failed',
|
|
24
|
+
'checkout.payment.expired',
|
|
25
|
+
];
|
|
26
|
+
export function createCoinbaseBusinessSubscription(config) {
|
|
27
|
+
const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
28
|
+
return {
|
|
29
|
+
async subscribe(options) {
|
|
30
|
+
const eventTypes = options.eventTypes ?? DEFAULT_EVENT_TYPES;
|
|
31
|
+
const jwt = await signCdpJwt({
|
|
32
|
+
keyId: config.cdpKeyId,
|
|
33
|
+
privateKeyPem: config.cdpPrivateKeyPem,
|
|
34
|
+
method: 'POST',
|
|
35
|
+
url: SUBSCRIPTION_URL,
|
|
36
|
+
});
|
|
37
|
+
const response = await fetcher(SUBSCRIPTION_URL, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${jwt}`,
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
callback_url: options.callbackUrl,
|
|
45
|
+
event_types: eventTypes,
|
|
46
|
+
environment: config.mode ?? 'live',
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const body = await response.text();
|
|
51
|
+
throw new Error(`coin-moebius/coinbase-business: subscription create failed (${response.status}): ${body}`);
|
|
52
|
+
}
|
|
53
|
+
const payload = (await response.json());
|
|
54
|
+
const subscriptionId = readString(payload, 'id', 'subscription_id');
|
|
55
|
+
const signingSecret = readString(payload, 'signing_secret', 'secret');
|
|
56
|
+
if (!subscriptionId || !signingSecret) {
|
|
57
|
+
throw new Error('coin-moebius/coinbase-business: subscription response missing id or signing_secret');
|
|
58
|
+
}
|
|
59
|
+
return { subscriptionId, signingSecret, raw: payload };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build and sign a CDP-flavored JWT. Exposed so callers integrating other
|
|
65
|
+
* CDP endpoints (Advanced Trade, Onchain) can reuse the same routine.
|
|
66
|
+
*
|
|
67
|
+
* The JWT pattern is documented at
|
|
68
|
+
* <https://docs.cdp.coinbase.com/get-started/authentication/jwt-authentication>:
|
|
69
|
+
*
|
|
70
|
+
* header = { alg: 'ES256', typ: 'JWT', kid: <keyId>, nonce: <random> }
|
|
71
|
+
* payload = { sub: <keyId>, iss: 'cdp', aud: ['cdp_service'],
|
|
72
|
+
* nbf: <now>, exp: <now + 120>, uri: '<METHOD> <host+path>' }
|
|
73
|
+
*/
|
|
74
|
+
export async function signCdpJwt(options) {
|
|
75
|
+
const now = Math.floor(Date.now() / 1000);
|
|
76
|
+
const parsedUrl = new URL(options.url);
|
|
77
|
+
const uriClaim = `${options.method.toUpperCase()} ${parsedUrl.host}${parsedUrl.pathname}`;
|
|
78
|
+
const header = {
|
|
79
|
+
alg: 'ES256',
|
|
80
|
+
typ: 'JWT',
|
|
81
|
+
kid: options.keyId,
|
|
82
|
+
nonce: randomNonce(),
|
|
83
|
+
};
|
|
84
|
+
const payload = {
|
|
85
|
+
sub: options.keyId,
|
|
86
|
+
iss: 'cdp',
|
|
87
|
+
aud: ['cdp_service'],
|
|
88
|
+
nbf: now,
|
|
89
|
+
exp: now + JWT_LIFETIME_SECONDS,
|
|
90
|
+
uri: uriClaim,
|
|
91
|
+
};
|
|
92
|
+
const signingInput = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(payload))}`;
|
|
93
|
+
const privateKey = await importEcPrivateKey(options.privateKeyPem);
|
|
94
|
+
const signatureBuf = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, privateKey, new TextEncoder().encode(signingInput));
|
|
95
|
+
// Web Crypto returns the IEEE P1363 raw r||s format, which is exactly
|
|
96
|
+
// what JOSE wants for ES256 (no DER decoding needed).
|
|
97
|
+
const signature = base64UrlEncodeBytes(new Uint8Array(signatureBuf));
|
|
98
|
+
return `${signingInput}.${signature}`;
|
|
99
|
+
}
|
|
100
|
+
async function importEcPrivateKey(pem) {
|
|
101
|
+
const pkcs8 = pemToBytes(pem);
|
|
102
|
+
return crypto.subtle.importKey('pkcs8', pkcs8, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
|
|
103
|
+
}
|
|
104
|
+
function pemToBytes(pem) {
|
|
105
|
+
const stripped = pem
|
|
106
|
+
.replace(/-----BEGIN [^-]+-----/, '')
|
|
107
|
+
.replace(/-----END [^-]+-----/, '')
|
|
108
|
+
.replace(/\s+/g, '');
|
|
109
|
+
if (!stripped) {
|
|
110
|
+
throw new Error('coin-moebius/coinbase-business: empty PEM payload');
|
|
111
|
+
}
|
|
112
|
+
const binary = atob(stripped);
|
|
113
|
+
const bytes = new Uint8Array(binary.length);
|
|
114
|
+
for (let i = 0; i < binary.length; i++)
|
|
115
|
+
bytes[i] = binary.charCodeAt(i);
|
|
116
|
+
return bytes;
|
|
117
|
+
}
|
|
118
|
+
function base64UrlEncode(input) {
|
|
119
|
+
return base64UrlEncodeBytes(new TextEncoder().encode(input));
|
|
120
|
+
}
|
|
121
|
+
function base64UrlEncodeBytes(bytes) {
|
|
122
|
+
let binary = '';
|
|
123
|
+
for (const b of bytes)
|
|
124
|
+
binary += String.fromCharCode(b);
|
|
125
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
126
|
+
}
|
|
127
|
+
function randomNonce() {
|
|
128
|
+
const bytes = new Uint8Array(16);
|
|
129
|
+
crypto.getRandomValues(bytes);
|
|
130
|
+
return base64UrlEncodeBytes(bytes);
|
|
131
|
+
}
|
|
132
|
+
function readString(obj, ...keys) {
|
|
133
|
+
for (const key of keys) {
|
|
134
|
+
const value = obj[key];
|
|
135
|
+
if (typeof value === 'string' && value.length > 0)
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=subscription.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription.js","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aquarian-metals/coin-moebius-coinbase-business",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Coinbase Business provider for Coin Moebius — hosted Checkout API redirect, plus a server-only Hook0 v1 webhook verifier and an optional programmatic webhook-subscription helper. Replaces the deprecated Coinbase Commerce surface.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"payments",
|
|
7
|
+
"coinbase",
|
|
8
|
+
"coinbase-business",
|
|
9
|
+
"crypto",
|
|
10
|
+
"checkout",
|
|
11
|
+
"webhook",
|
|
12
|
+
"hook0",
|
|
13
|
+
"coin-moebius"
|
|
14
|
+
],
|
|
15
|
+
"author": "aquarian-metals",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/aquarian-metals/coin-moebius#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/aquarian-metals/coin-moebius/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/aquarian-metals/coin-moebius.git",
|
|
24
|
+
"directory": "packages/providers/coinbase-business"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./server": {
|
|
39
|
+
"types": "./dist/server.d.ts",
|
|
40
|
+
"import": "./dist/server.js"
|
|
41
|
+
},
|
|
42
|
+
"./subscription": {
|
|
43
|
+
"types": "./dist/subscription.d.ts",
|
|
44
|
+
"import": "./dist/subscription.js"
|
|
45
|
+
},
|
|
46
|
+
"./package.json": "./package.json"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
50
|
+
"build": "npm run clean && tsc",
|
|
51
|
+
"prepublishOnly": "npm run build"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@aquarian-metals/coin-moebius-core": "^1.0.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"typescript": "~5.8.0"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
}
|
|
65
|
+
}
|