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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpay-x402-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Guarded Node.js buyer SDK for OpenPay x402 JPYC resources",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
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
+ }
package/src/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './catalog.mjs';
2
2
  export * from './client.mjs';
3
3
  export * from './executor.mjs';
4
+ export * from './gate.mjs';
4
5
  export * from './guards.mjs';
5
6
  export * from './payment.mjs';
6
7
  export * from './signer.mjs';