@phala/pay 0.2.0 → 0.3.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/CHANGELOG.md +21 -1
- package/README.md +28 -27
- package/dist/quote.js +2 -2
- package/dist/quote.js.map +1 -1
- package/dist/server/webhook.d.ts +6 -5
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +18 -16
- package/dist/server/webhook.js.map +1 -1
- package/package.json +1 -1
- package/src/quote.ts +2 -2
- package/src/server/webhook.ts +21 -19
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ All notable changes to `@phala/pay` are recorded here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
### Changed (breaking)
|
|
12
|
+
|
|
13
|
+
- `constructEvent` takes webhook public keys only in Standard Webhooks' `whpk_` form (`whpk_` and
|
|
14
|
+
the standard base64 of the key), as `GET /v1/attestation` lists them; a hex or bare base64 key
|
|
15
|
+
is refused.
|
|
16
|
+
- `parseClientQuote` requires `confirmations`, an integer or `null`, as the service always sends
|
|
17
|
+
it.
|
|
18
|
+
- `constructEvent` requires the event's `actor` and `request` (`null` or the causing request), as
|
|
19
|
+
the service always sends them, and refuses an envelope without them; `WebhookEvent.actor` and
|
|
20
|
+
`request` are no longer optional.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `constructEvent` returns the event's `request` (the causing API request's `id` and
|
|
25
|
+
`idempotency_key`, or `null`), as `WebhookEvent` declares; it was dropped. A malformed `request`
|
|
26
|
+
is refused like any malformed event.
|
|
27
|
+
|
|
9
28
|
## [0.2.0] - 2026-09-29
|
|
10
29
|
|
|
11
30
|
### Changed (breaking)
|
|
@@ -133,7 +152,8 @@ All notable changes to `@phala/pay` are recorded here. The format follows
|
|
|
133
152
|
without trailing zeros (`1,273.9185`); `tokenAmount(quote)` is the plain decimal a wallet
|
|
134
153
|
accepts, which `<Checkout>`'s "Exact amount" copies.
|
|
135
154
|
|
|
136
|
-
[
|
|
155
|
+
[Unreleased]: https://github.com/Phala-Network/phala-pay/compare/sdk-js-v0.3.0...HEAD
|
|
156
|
+
[0.3.0]: https://github.com/Phala-Network/phala-pay/compare/sdk-js-v0.2.0...sdk-js-v0.3.0
|
|
137
157
|
[0.2.0]: https://github.com/Phala-Network/phala-pay/compare/sdk-js-v0.1.2...sdk-js-v0.2.0
|
|
138
158
|
[0.1.2]: https://github.com/Phala-Network/phala-pay/compare/sdk-js-v0.1.1...sdk-js-v0.1.2
|
|
139
159
|
[0.1.1]: https://github.com/Phala-Network/phala-pay/compare/sdk-js-v0.1.0...sdk-js-v0.1.1
|
package/README.md
CHANGED
|
@@ -19,34 +19,34 @@ connects a wallet with wagmi, RainbowKit, ConnectKit, or AppKit, pass it in inst
|
|
|
19
19
|
|
|
20
20
|
## Quickstart
|
|
21
21
|
|
|
22
|
-
1. Your backend creates a quote (`POST /v1/quotes` with its
|
|
22
|
+
1. Your backend creates a quote (`POST /v1/quotes` with its API key, for example
|
|
23
23
|
`phala-pay`'s `pay.quotes.create`), recomputes its address from the pinned forwarder (the
|
|
24
24
|
Python SDK does it on every call), and returns only its `client_secret` and that address to
|
|
25
25
|
the signed-in user's browser.
|
|
26
26
|
2. Render the checkout with them:
|
|
27
27
|
|
|
28
|
-
```tsx
|
|
29
|
-
"use client";
|
|
30
|
-
import { Checkout } from "@phala/pay/react";
|
|
31
|
-
|
|
32
|
-
export function TopUp(props: {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
```
|
|
28
|
+
```tsx
|
|
29
|
+
"use client";
|
|
30
|
+
import { Checkout } from "@phala/pay/react";
|
|
31
|
+
|
|
32
|
+
export function TopUp(props: {
|
|
33
|
+
clientSecret: string;
|
|
34
|
+
expectedAddress: string;
|
|
35
|
+
onPaid: () => void;
|
|
36
|
+
onRetry: () => void;
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<Checkout
|
|
40
|
+
clientSecret={props.clientSecret}
|
|
41
|
+
expectedAddress={props.expectedAddress}
|
|
42
|
+
apiBase="https://pay.example.com"
|
|
43
|
+
onSuccess={props.onPaid}
|
|
44
|
+
onExpire={props.onRetry}
|
|
45
|
+
buttonText="Pay with crypto"
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
50
|
|
|
51
51
|
3. Credit the account when your webhook endpoint receives `deposit.credited`. `onSuccess` is for the
|
|
52
52
|
UI only: the browser is not a trusted source of payment.
|
|
@@ -102,12 +102,12 @@ ConnectKit, and AppKit, that is `useWalletClient()`:
|
|
|
102
102
|
import { Checkout } from "@phala/pay/react";
|
|
103
103
|
import { useWalletClient } from "wagmi";
|
|
104
104
|
|
|
105
|
-
export function TopUp({ clientSecret
|
|
105
|
+
export function TopUp(props: { clientSecret: string; expectedAddress: string }) {
|
|
106
106
|
const { data: walletClient } = useWalletClient();
|
|
107
107
|
return (
|
|
108
108
|
<Checkout
|
|
109
|
-
clientSecret={clientSecret}
|
|
110
|
-
expectedAddress={expectedAddress}
|
|
109
|
+
clientSecret={props.clientSecret}
|
|
110
|
+
expectedAddress={props.expectedAddress}
|
|
111
111
|
apiBase="https://pay.example.com"
|
|
112
112
|
walletClient={walletClient}
|
|
113
113
|
/>
|
|
@@ -121,7 +121,7 @@ as usual. The QR code and manual tabs do not change.
|
|
|
121
121
|
### A customer's deposit address
|
|
122
122
|
|
|
123
123
|
For top-ups of any amount, your backend creates the customer's persistent deposit address with
|
|
124
|
-
`POST /v1/deposit_addresses` (
|
|
124
|
+
`POST /v1/deposit_addresses` (its API key) and passes its `address` and `networks` to the page:
|
|
125
125
|
one address for all supported tokens and networks. `<DepositAddress>` lets the payer pick a
|
|
126
126
|
network and a token and shows a QR code of that token's transfer request, the token contract, and
|
|
127
127
|
the address to copy, with "send only supported tokens" and "credited at the market rate when it
|
|
@@ -232,6 +232,7 @@ const expectedAddress = verifyQuoteAddress(pins, quote); // pass it to <Checkout
|
|
|
232
232
|
|
|
233
233
|
// Standard Webhooks v1a (ed25519, WebCrypto: Node 20+, Deno, Bun, edge runtimes). Fails closed
|
|
234
234
|
// unless the signature verifies with a pinned key and the event is your account's in this mode.
|
|
235
|
+
// WEBHOOK_PUBLIC_KEYS: your account's keys in this mode, `whpk_…`, pinned from GET /v1/attestation.
|
|
235
236
|
const event = await constructEvent(rawBody, request.headers, WEBHOOK_PUBLIC_KEYS, {
|
|
236
237
|
expectedAccount: "acct_…",
|
|
237
238
|
expectedLivemode: false,
|
package/dist/quote.js
CHANGED
|
@@ -31,7 +31,7 @@ export function parseClientQuote(value) {
|
|
|
31
31
|
typeof v["payment_uri"] !== "string" ||
|
|
32
32
|
!isSafeInteger(v["expires_at"]) ||
|
|
33
33
|
!oneOf(v["payment_status"], PAYMENT_STATUSES) ||
|
|
34
|
-
!(confirmations === null ||
|
|
34
|
+
!(confirmations === null || isSafeInteger(confirmations))) {
|
|
35
35
|
throw new TypeError("quote response does not match the public quote view");
|
|
36
36
|
}
|
|
37
37
|
return {
|
|
@@ -49,7 +49,7 @@ export function parseClientQuote(value) {
|
|
|
49
49
|
payment_uri: v["payment_uri"],
|
|
50
50
|
expires_at: v["expires_at"],
|
|
51
51
|
payment_status: v["payment_status"],
|
|
52
|
-
confirmations
|
|
52
|
+
confirmations,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
function oneOf(value, allowed) {
|
package/dist/quote.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quote.js","sourceRoot":"","sources":["../src/quote.ts"],"names":[],"mappings":"AAoCA,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAC5E,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,CAAU,CAAC;AAErG,oFAAoF;AACpF,MAAM,UAAU,uBAAuB,CAAC,YAAoB;IAC1D,MAAM,KAAK,GAAG,mCAAmC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACrE,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,MAAM,IAAI,SAAS,CAAC,iCAAiC,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,MAAM,aAAa,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC;IACzC,IACE,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,QAAQ;QAC3B,CAAC,CAAC,QAAQ,CAAC,KAAK,OAAO;QACvB,OAAO,CAAC,CAAC,UAAU,CAAC,KAAK,SAAS;QAClC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;QACnC,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC3B,OAAO,CAAC,CAAC,UAAU,CAAC,KAAK,QAAQ;QACjC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ;QAC9B,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QAC7B,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QAC7B,OAAO,CAAC,CAAC,eAAe,CAAC,KAAK,QAAQ;QACtC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;QACjC,OAAO,CAAC,CAAC,SAAS,CAAC,KAAK,QAAQ;QAChC,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACzC,OAAO,CAAC,CAAC,aAAa,CAAC,KAAK,QAAQ;QACpC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;QAC/B,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;QAC7C,CAAC,CAAC,aAAa,KAAK,IAAI,IAAI,aAAa,
|
|
1
|
+
{"version":3,"file":"quote.js","sourceRoot":"","sources":["../src/quote.ts"],"names":[],"mappings":"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"}
|
package/dist/server/webhook.d.ts
CHANGED
|
@@ -20,12 +20,12 @@ export interface WebhookEvent {
|
|
|
20
20
|
type: string;
|
|
21
21
|
created: number;
|
|
22
22
|
/** Who caused it: an API key id (`key_…`), `admin`, or `system`. */
|
|
23
|
-
actor
|
|
23
|
+
actor: string;
|
|
24
24
|
/**
|
|
25
25
|
* The API request that caused it, with the `Idempotency-Key` it sent; `null` when the
|
|
26
26
|
* service's own workers did (a payment credited, a quote expired).
|
|
27
27
|
*/
|
|
28
|
-
request
|
|
28
|
+
request: {
|
|
29
29
|
id: string;
|
|
30
30
|
idempotency_key: string | null;
|
|
31
31
|
} | null;
|
|
@@ -50,9 +50,10 @@ export interface ConstructEventOptions {
|
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Verifies a delivery and returns its event, as Stripe's `constructEvent` does, failing closed
|
|
53
|
-
* unless a `v1a` signature verifies with one of `publicKeys` (
|
|
54
|
-
*
|
|
55
|
-
* `webhook-id`, and the event is
|
|
53
|
+
* unless a `v1a` signature verifies with one of `publicKeys` (ed25519 keys in Standard Webhooks'
|
|
54
|
+
* `whpk_` form, as `GET /v1/attestation` lists them; pass both while a rotation overlaps), the
|
|
55
|
+
* timestamp is within tolerance, the body's id is the `webhook-id`, and the event is
|
|
56
|
+
* `expectedAccount`'s in `expectedLivemode`.
|
|
56
57
|
*
|
|
57
58
|
* `payload` is the raw request body, before any JSON parsing.
|
|
58
59
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,mEAAmE;AACnE,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;CAClD;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,KAAK,
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,mEAAmE;AACnE,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;CAClD;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/D;;;OAGG;IACH,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;CAC1F;AAED,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EAChE,UAAU,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACtC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAgGvB"}
|
package/dist/server/webhook.js
CHANGED
|
@@ -10,9 +10,10 @@ export class WebhookSignatureError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Verifies a delivery and returns its event, as Stripe's `constructEvent` does, failing closed
|
|
13
|
-
* unless a `v1a` signature verifies with one of `publicKeys` (
|
|
14
|
-
*
|
|
15
|
-
* `webhook-id`, and the event is
|
|
13
|
+
* unless a `v1a` signature verifies with one of `publicKeys` (ed25519 keys in Standard Webhooks'
|
|
14
|
+
* `whpk_` form, as `GET /v1/attestation` lists them; pass both while a rotation overlaps), the
|
|
15
|
+
* timestamp is within tolerance, the body's id is the `webhook-id`, and the event is
|
|
16
|
+
* `expectedAccount`'s in `expectedLivemode`.
|
|
16
17
|
*
|
|
17
18
|
* `payload` is the raw request body, before any JSON parsing.
|
|
18
19
|
*/
|
|
@@ -68,7 +69,7 @@ export async function constructEvent(payload, headers, publicKeys, options) {
|
|
|
68
69
|
const e = event;
|
|
69
70
|
const data = e["data"];
|
|
70
71
|
const object = typeof data === "object" && data !== null ? data["object"] : null;
|
|
71
|
-
const { id: eventId, type, account, livemode, created, actor } = e;
|
|
72
|
+
const { id: eventId, type, account, livemode, created, actor, request } = e;
|
|
72
73
|
if (e["object"] !== "event" ||
|
|
73
74
|
typeof eventId !== "string" ||
|
|
74
75
|
typeof type !== "string" ||
|
|
@@ -78,7 +79,8 @@ export async function constructEvent(payload, headers, publicKeys, options) {
|
|
|
78
79
|
!Number.isSafeInteger(created) ||
|
|
79
80
|
typeof object !== "object" ||
|
|
80
81
|
object === null ||
|
|
81
|
-
|
|
82
|
+
typeof actor !== "string" ||
|
|
83
|
+
!(request === null || isRequest(request))) {
|
|
82
84
|
throw new TypeError("webhook body is not an event");
|
|
83
85
|
}
|
|
84
86
|
if (eventId !== id) {
|
|
@@ -98,7 +100,8 @@ export async function constructEvent(payload, headers, publicKeys, options) {
|
|
|
98
100
|
livemode,
|
|
99
101
|
type,
|
|
100
102
|
created,
|
|
101
|
-
|
|
103
|
+
actor,
|
|
104
|
+
request,
|
|
102
105
|
data: {
|
|
103
106
|
object: object,
|
|
104
107
|
...(typeof previous === "object" && previous !== null
|
|
@@ -118,21 +121,20 @@ function header(headers, name) {
|
|
|
118
121
|
}
|
|
119
122
|
return undefined;
|
|
120
123
|
}
|
|
124
|
+
function isRequest(value) {
|
|
125
|
+
if (typeof value !== "object" || value === null) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const { id, idempotency_key: key } = value;
|
|
129
|
+
return typeof id === "string" && (key === null || typeof key === "string");
|
|
130
|
+
}
|
|
121
131
|
async function importKey(encoded) {
|
|
122
|
-
const
|
|
123
|
-
const raw = /^[0-9a-fA-F]{64}$/.test(value) ? hex(value) : base64(value);
|
|
132
|
+
const raw = encoded.startsWith("whpk_") ? base64(encoded.slice("whpk_".length)) : undefined;
|
|
124
133
|
if (raw?.length !== 32) {
|
|
125
|
-
throw new TypeError("a webhook public key is
|
|
134
|
+
throw new TypeError("a webhook public key is whpk_ and the base64 of 32 bytes");
|
|
126
135
|
}
|
|
127
136
|
return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
|
|
128
137
|
}
|
|
129
|
-
function hex(value) {
|
|
130
|
-
const bytes = new Uint8Array(value.length / 2);
|
|
131
|
-
for (let index = 0; index < bytes.length; index += 1) {
|
|
132
|
-
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
|
133
|
-
}
|
|
134
|
-
return bytes;
|
|
135
|
-
}
|
|
136
138
|
function base64(value) {
|
|
137
139
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
138
140
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,mEAAmE;AACnE,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC5B,IAAI,GAAG,uBAAuB,CAAC;CAClD;AAsCD
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/quote.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function parseClientQuote(value: unknown): ClientQuote {
|
|
|
70
70
|
typeof v["payment_uri"] !== "string" ||
|
|
71
71
|
!isSafeInteger(v["expires_at"]) ||
|
|
72
72
|
!oneOf(v["payment_status"], PAYMENT_STATUSES) ||
|
|
73
|
-
!(confirmations === null ||
|
|
73
|
+
!(confirmations === null || isSafeInteger(confirmations))
|
|
74
74
|
) {
|
|
75
75
|
throw new TypeError("quote response does not match the public quote view");
|
|
76
76
|
}
|
|
@@ -89,7 +89,7 @@ export function parseClientQuote(value: unknown): ClientQuote {
|
|
|
89
89
|
payment_uri: v["payment_uri"],
|
|
90
90
|
expires_at: v["expires_at"],
|
|
91
91
|
payment_status: v["payment_status"],
|
|
92
|
-
confirmations
|
|
92
|
+
confirmations,
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
|
package/src/server/webhook.ts
CHANGED
|
@@ -22,12 +22,12 @@ export interface WebhookEvent {
|
|
|
22
22
|
type: string;
|
|
23
23
|
created: number;
|
|
24
24
|
/** Who caused it: an API key id (`key_…`), `admin`, or `system`. */
|
|
25
|
-
actor
|
|
25
|
+
actor: string;
|
|
26
26
|
/**
|
|
27
27
|
* The API request that caused it, with the `Idempotency-Key` it sent; `null` when the
|
|
28
28
|
* service's own workers did (a payment credited, a quote expired).
|
|
29
29
|
*/
|
|
30
|
-
request
|
|
30
|
+
request: { id: string; idempotency_key: string | null } | null;
|
|
31
31
|
/**
|
|
32
32
|
* `object` is the object as it was when the event happened, never re-rendered; `*.updated`
|
|
33
33
|
* events add `previous_attributes`, the former values of what changed.
|
|
@@ -48,9 +48,10 @@ export interface ConstructEventOptions {
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Verifies a delivery and returns its event, as Stripe's `constructEvent` does, failing closed
|
|
51
|
-
* unless a `v1a` signature verifies with one of `publicKeys` (
|
|
52
|
-
*
|
|
53
|
-
* `webhook-id`, and the event is
|
|
51
|
+
* unless a `v1a` signature verifies with one of `publicKeys` (ed25519 keys in Standard Webhooks'
|
|
52
|
+
* `whpk_` form, as `GET /v1/attestation` lists them; pass both while a rotation overlaps), the
|
|
53
|
+
* timestamp is within tolerance, the body's id is the `webhook-id`, and the event is
|
|
54
|
+
* `expectedAccount`'s in `expectedLivemode`.
|
|
54
55
|
*
|
|
55
56
|
* `payload` is the raw request body, before any JSON parsing.
|
|
56
57
|
*/
|
|
@@ -113,7 +114,7 @@ export async function constructEvent(
|
|
|
113
114
|
const data = e["data"];
|
|
114
115
|
const object: unknown =
|
|
115
116
|
typeof data === "object" && data !== null ? (data as Record<string, unknown>)["object"] : null;
|
|
116
|
-
const { id: eventId, type, account, livemode, created, actor } = e;
|
|
117
|
+
const { id: eventId, type, account, livemode, created, actor, request } = e;
|
|
117
118
|
if (
|
|
118
119
|
e["object"] !== "event" ||
|
|
119
120
|
typeof eventId !== "string" ||
|
|
@@ -124,7 +125,8 @@ export async function constructEvent(
|
|
|
124
125
|
!Number.isSafeInteger(created) ||
|
|
125
126
|
typeof object !== "object" ||
|
|
126
127
|
object === null ||
|
|
127
|
-
|
|
128
|
+
typeof actor !== "string" ||
|
|
129
|
+
!(request === null || isRequest(request))
|
|
128
130
|
) {
|
|
129
131
|
throw new TypeError("webhook body is not an event");
|
|
130
132
|
}
|
|
@@ -145,7 +147,8 @@ export async function constructEvent(
|
|
|
145
147
|
livemode,
|
|
146
148
|
type,
|
|
147
149
|
created,
|
|
148
|
-
|
|
150
|
+
actor,
|
|
151
|
+
request,
|
|
149
152
|
data: {
|
|
150
153
|
object: object as Record<string, unknown>,
|
|
151
154
|
...(typeof previous === "object" && previous !== null
|
|
@@ -170,21 +173,20 @@ function header(
|
|
|
170
173
|
return undefined;
|
|
171
174
|
}
|
|
172
175
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (raw?.length !== 32) {
|
|
177
|
-
throw new TypeError("a webhook public key is 32 bytes of hex or base64");
|
|
176
|
+
function isRequest(value: unknown): value is { id: string; idempotency_key: string | null } {
|
|
177
|
+
if (typeof value !== "object" || value === null) {
|
|
178
|
+
return false;
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
+
const { id, idempotency_key: key } = value as Record<string, unknown>;
|
|
181
|
+
return typeof id === "string" && (key === null || typeof key === "string");
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
function
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
async function importKey(encoded: string): Promise<CryptoKey> {
|
|
185
|
+
const raw = encoded.startsWith("whpk_") ? base64(encoded.slice("whpk_".length)) : undefined;
|
|
186
|
+
if (raw?.length !== 32) {
|
|
187
|
+
throw new TypeError("a webhook public key is whpk_ and the base64 of 32 bytes");
|
|
186
188
|
}
|
|
187
|
-
return
|
|
189
|
+
return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
function base64(value: string): Uint8Array<ArrayBuffer> | undefined {
|