openpay-x402-sdk 0.2.1 → 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 +8 -0
- package/README.md +61 -0
- package/index.d.ts +22 -0
- package/package.json +1 -1
- package/src/gate.mjs +119 -0
- package/src/index.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Add `createJpycGate` for seller-side x402 gates backed by the OpenPay catalog,
|
|
6
|
+
including five-minute `accepts` caching and request-specific resource URLs.
|
|
7
|
+
- Support both one-shot verify-to-settle handling and split verification followed
|
|
8
|
+
by settlement after an expensive upstream operation succeeds.
|
|
9
|
+
- Use Edge-compatible UTF-8 base64 handling for payment and settlement headers.
|
|
10
|
+
|
|
3
11
|
## 0.2.1
|
|
4
12
|
|
|
5
13
|
- Compare `accept.resource` against the requested URL using decoded query
|
package/README.md
CHANGED
|
@@ -32,6 +32,67 @@ inside `{ ok, status, body }`. `quote()` fetches and validates a 402 challenge b
|
|
|
32
32
|
does not need a signer and never pays. `pay()` requires a signer and serializes
|
|
33
33
|
concurrent calls so every call sees the latest session total.
|
|
34
34
|
|
|
35
|
+
## Sell with the SDK
|
|
36
|
+
|
|
37
|
+
Create a gate with the exact resource URL registered in OpenPay discovery. For
|
|
38
|
+
inexpensive content, `handle()` verifies and settles the payment in one call:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { createJpycGate } from 'openpay-x402-sdk';
|
|
42
|
+
|
|
43
|
+
const gate = createJpycGate({
|
|
44
|
+
resourceUrl: process.env.MY_RESOURCE_URL,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export async function GET(request) {
|
|
48
|
+
const payment = await gate.handle(request);
|
|
49
|
+
if (payment instanceof Response) return payment;
|
|
50
|
+
|
|
51
|
+
const response = Response.json({ your: 'paid content' });
|
|
52
|
+
response.headers.set(
|
|
53
|
+
'X-PAYMENT-RESPONSE',
|
|
54
|
+
payment.paymentResponseHeader,
|
|
55
|
+
);
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For an expensive upstream operation, verify first and settle only after the
|
|
61
|
+
operation succeeds. If `callUpstream()` fails, return an error before calling
|
|
62
|
+
`settle()` so the buyer remains uncharged:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
export async function GET(request) {
|
|
66
|
+
const payment = await gate.verify(request);
|
|
67
|
+
if (payment instanceof Response) return payment;
|
|
68
|
+
|
|
69
|
+
let data;
|
|
70
|
+
try {
|
|
71
|
+
data = await callUpstream();
|
|
72
|
+
} catch {
|
|
73
|
+
return Response.json({ error: 'upstream_failed' }, { status: 502 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const settlement = await payment.settle();
|
|
77
|
+
if (settlement instanceof Response) return settlement;
|
|
78
|
+
|
|
79
|
+
const response = Response.json(data);
|
|
80
|
+
response.headers.set(
|
|
81
|
+
'X-PAYMENT-RESPONSE',
|
|
82
|
+
settlement.paymentResponseHeader,
|
|
83
|
+
);
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`createJpycGate` fetches `accepts` from `/api/discovery` and caches it for five
|
|
89
|
+
minutes. Until `resourceUrl` is listed with a non-empty `accepts`, `handle()` and
|
|
90
|
+
`verify()` throw; map that bootstrap condition to an HTTP 500 response. Pass
|
|
91
|
+
`openpayOrigin` to use an origin other than `https://open-pay.jp`.
|
|
92
|
+
|
|
93
|
+
The copy-paste paywall snippet generated by OpenPay provides the same one-shot
|
|
94
|
+
gate; `createJpycGate` is its importable SDK counterpart with split settlement.
|
|
95
|
+
|
|
35
96
|
## Money guards
|
|
36
97
|
|
|
37
98
|
| Option | Default | Guard |
|
package/index.d.ts
CHANGED
|
@@ -194,6 +194,28 @@ export function createOpenPayClient(
|
|
|
194
194
|
options?: OpenPayClientOptions,
|
|
195
195
|
): OpenPayClient;
|
|
196
196
|
|
|
197
|
+
export interface JpycGateOptions {
|
|
198
|
+
resourceUrl: string;
|
|
199
|
+
openpayOrigin?: string;
|
|
200
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
201
|
+
now?: () => number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface JpycGatePaymentResponse {
|
|
205
|
+
paymentResponseHeader: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface VerifiedJpycPayment {
|
|
209
|
+
settle(): Promise<Response | JpycGatePaymentResponse>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface JpycGate {
|
|
213
|
+
handle(request: Request): Promise<Response | JpycGatePaymentResponse>;
|
|
214
|
+
verify(request: Request): Promise<Response | VerifiedJpycPayment>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createJpycGate(options: JpycGateOptions): JpycGate;
|
|
218
|
+
|
|
197
219
|
export const RECEIVE_WITH_AUTHORIZATION_TYPES: {
|
|
198
220
|
ReceiveWithAuthorization: Array<{ name: string; type: string }>;
|
|
199
221
|
};
|
package/package.json
CHANGED
package/src/gate.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const DEFAULT_OPENPAY_ORIGIN = 'https://open-pay.jp';
|
|
2
|
+
const ACCEPTS_CACHE_MS = 5 * 60_000;
|
|
3
|
+
|
|
4
|
+
function json402(accepts, error) {
|
|
5
|
+
return new Response(JSON.stringify({ x402Version: 1, accepts, error }), {
|
|
6
|
+
status: 402,
|
|
7
|
+
headers: { 'content-type': 'application/json' },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function decodeBase64Json(value) {
|
|
12
|
+
const binary = atob(value);
|
|
13
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
14
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function encodeBase64Json(value) {
|
|
18
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
19
|
+
let binary = '';
|
|
20
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
21
|
+
return btoa(binary);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createJpycGate({
|
|
25
|
+
resourceUrl,
|
|
26
|
+
openpayOrigin = DEFAULT_OPENPAY_ORIGIN,
|
|
27
|
+
fetchImpl = globalThis.fetch,
|
|
28
|
+
now = Date.now,
|
|
29
|
+
}) {
|
|
30
|
+
const origin = openpayOrigin.replace(/\/+$/, '');
|
|
31
|
+
let acceptsCache = null;
|
|
32
|
+
let acceptsCachedAt = 0;
|
|
33
|
+
|
|
34
|
+
async function catalogAccepts() {
|
|
35
|
+
if (
|
|
36
|
+
acceptsCache !== null &&
|
|
37
|
+
now() - acceptsCachedAt < ACCEPTS_CACHE_MS
|
|
38
|
+
) {
|
|
39
|
+
return acceptsCache;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const response = await fetchImpl(`${origin}/api/discovery`);
|
|
43
|
+
const { items } = await response.json();
|
|
44
|
+
const mine = (items || []).find((item) => item.resource === resourceUrl);
|
|
45
|
+
if (!mine || !mine.accepts || mine.accepts.length === 0) {
|
|
46
|
+
throw new Error(`resource not found in OpenPay catalog: ${resourceUrl}`);
|
|
47
|
+
}
|
|
48
|
+
acceptsCache = mine.accepts;
|
|
49
|
+
acceptsCachedAt = now();
|
|
50
|
+
return acceptsCache;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function facilitator(path, paymentPayload, paymentRequirements) {
|
|
54
|
+
const response = await fetchImpl(`${origin}/api/facilitator/${path}`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'content-type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
x402Version: 1,
|
|
59
|
+
paymentPayload,
|
|
60
|
+
paymentRequirements,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
return response.json();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function verify(request) {
|
|
67
|
+
const accepts = (await catalogAccepts()).map((accept) => ({
|
|
68
|
+
...accept,
|
|
69
|
+
resource: request.url,
|
|
70
|
+
}));
|
|
71
|
+
const header = request.headers.get('x-payment');
|
|
72
|
+
if (!header) return json402(accepts, 'payment_required');
|
|
73
|
+
|
|
74
|
+
let paymentPayload;
|
|
75
|
+
try {
|
|
76
|
+
paymentPayload = decodeBase64Json(header);
|
|
77
|
+
} catch {
|
|
78
|
+
return json402(accepts, 'invalid_payment_payload');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const paymentRequirements = accepts[0];
|
|
82
|
+
const verification = await facilitator(
|
|
83
|
+
'verify',
|
|
84
|
+
paymentPayload,
|
|
85
|
+
paymentRequirements,
|
|
86
|
+
);
|
|
87
|
+
if (verification.isValid !== true) {
|
|
88
|
+
return json402(
|
|
89
|
+
accepts,
|
|
90
|
+
verification.invalidReason || 'payment_invalid',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
async settle() {
|
|
96
|
+
const settlement = await facilitator(
|
|
97
|
+
'settle',
|
|
98
|
+
paymentPayload,
|
|
99
|
+
paymentRequirements,
|
|
100
|
+
);
|
|
101
|
+
if (settlement.success !== true) {
|
|
102
|
+
return json402(
|
|
103
|
+
accepts,
|
|
104
|
+
settlement.errorReason || 'settlement_failed',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return { paymentResponseHeader: encodeBase64Json(settlement) };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handle(request) {
|
|
113
|
+
const verification = await verify(request);
|
|
114
|
+
if (verification instanceof Response) return verification;
|
|
115
|
+
return verification.settle();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { handle, verify };
|
|
119
|
+
}
|