openpay-x402-sdk 0.4.0 → 0.6.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 +67 -0
- package/README.md +134 -19
- package/index.d.ts +238 -0
- package/package.json +1 -1
- package/src/catalog.mjs +27 -3
- package/src/client.mjs +16 -1
- package/src/dualGate.mjs +196 -0
- package/src/executor.mjs +417 -57
- package/src/gate.mjs +151 -6
- package/src/guards.mjs +123 -27
- package/src/index.mjs +4 -0
- package/src/listing.mjs +173 -0
- package/src/network.mjs +273 -0
- package/src/payment.mjs +20 -1
- package/src/receipt.mjs +201 -0
- package/src/spendStore.mjs +411 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
- Add `createDualGate` — a dual-rail seller gate that serves both JPYC (Polygon,
|
|
6
|
+
OpenPay facilitator) and USDC (Base, standard x402 relayed via OpenPay to the
|
|
7
|
+
CDP facilitator). USDC payments settle directly to the seller wallet with 0%
|
|
8
|
+
OpenPay fee; if the USDC face cannot be fetched, the gate degrades to
|
|
9
|
+
JPYC-only and never blocks JPYC payments.
|
|
10
|
+
- Add `createListingClient` — programmatic marketplace listing (register, list,
|
|
11
|
+
update, deactivate) with built-in SIWE sign-in, so sellers and agents can
|
|
12
|
+
publish listings without the web form. `register` requires an explicit
|
|
13
|
+
`attested: true` (the SDK never attests on your behalf); set `usdc` to also
|
|
14
|
+
appear on the x402 Bazaar after the first settled purchase.
|
|
15
|
+
- Return an explicit `settlement` field from `pay()`, because HTTP `200` only
|
|
16
|
+
means the seller returned a body and is not evidence that the payment settled.
|
|
17
|
+
`verified` means a receipt header was present and the facilitator signature
|
|
18
|
+
bound it to this payment, `unverified` means a header was present but
|
|
19
|
+
unsigned, malformed, forged, or mismatched, and `receipt_unavailable` means no
|
|
20
|
+
header was returned or the facilitator signer could not be resolved. Treat
|
|
21
|
+
anything but `verified` as not proven paid. `receipt` keeps its previous
|
|
22
|
+
meaning and an unlocked response body is still never discarded.
|
|
23
|
+
- Take over a spend lock left behind by a killed process instead of failing
|
|
24
|
+
budgeted payments forever. A lock whose last modification is older than
|
|
25
|
+
`SPEND_LOCK_STALE_MS` (60s, now exported) is moved aside with an atomic
|
|
26
|
+
`rename` — never `unlink` — and the mover re-inspects the moved file to prove
|
|
27
|
+
it took the very lock it measured, so two processes that observe the same
|
|
28
|
+
stale lock cannot both enter the critical section. A lock younger than the
|
|
29
|
+
window is left alone, so a live holder is never displaced.
|
|
30
|
+
- Record the owning `pid` and `createdAt` in the lock file, name the lock path in
|
|
31
|
+
a new `detail` on `{ ok: false, reason: 'unavailable' }`, and treat an
|
|
32
|
+
already-absent lock at release time as a completed release. A custom `fsImpl`
|
|
33
|
+
without `stat`/`rename` keeps the previous fail-closed behavior and now warns
|
|
34
|
+
once instead of disabling the takeover silently.
|
|
35
|
+
- Resolve the target hostname before calling an injected custom `fetchImpl`, so a
|
|
36
|
+
public name pointing at a private or link-local address is rejected before the
|
|
37
|
+
custom transport runs. Connection-time rebinding protection still requires
|
|
38
|
+
supplying `lookup`; a resolver failure does not block, since only the transport
|
|
39
|
+
that opens the socket can re-validate the address it connects to.
|
|
40
|
+
- Require `DISCOVERY_URL` to be `https`, with plaintext `http` allowed only for
|
|
41
|
+
`localhost` / `127.0.0.1`. The discovery origin is the authority for catalog
|
|
42
|
+
trust — URLs it lists are payable without an `ALLOWED_HOSTS` entry — so a
|
|
43
|
+
substitutable plaintext catalog could pass an attacker's resource off as
|
|
44
|
+
reviewed. Both `readRuntimeConfig` and `parseClientOptions` enforce it.
|
|
45
|
+
- Declare the new surface in `index.d.ts` (`SETTLEMENT`, `SettlementStatus`,
|
|
46
|
+
`PaymentResult.settlement`, `SPEND_LOCK_STALE_MS`, `SpendReservationResult.detail`,
|
|
47
|
+
`fsImpl.stat`, `createDualGate`, `createListingClient` and their inputs) and
|
|
48
|
+
document dual-rail selling, code-side listing, settlement truth, stale-lock
|
|
49
|
+
takeover, and the custom-transport boundary in the README.
|
|
50
|
+
|
|
51
|
+
## 0.5.0
|
|
52
|
+
|
|
53
|
+
- Reserve session and daily capacity immediately before exposing a signed
|
|
54
|
+
authorization. Non-2xx responses, timeouts, and connection failures retain
|
|
55
|
+
the reservation; successful 2xx responses keep the existing confirmed-spend
|
|
56
|
+
accounting.
|
|
57
|
+
- Make the file daily store cross-process atomic with an exclusive lock, reject
|
|
58
|
+
UTC-crossing authorizations, and fail closed when a configured store is
|
|
59
|
+
unavailable.
|
|
60
|
+
- Enforce host/catalog admission before target I/O, block private and rebinding
|
|
61
|
+
destinations, require exact catalog URLs, stop redirects, and bound buyer
|
|
62
|
+
requests with a timeout.
|
|
63
|
+
- Bind supported networks to the canonical JPYC v3 contract/domain, cap
|
|
64
|
+
seller-declared authorization lifetimes, bind signature destinations to a
|
|
65
|
+
known or catalog-reviewed forwarder, and locally reserve seller-gate
|
|
66
|
+
authorizations across verify and settle without requiring facilitator tokens.
|
|
67
|
+
- Verify facilitator-signed payment receipts against the advertised signer and
|
|
68
|
+
bind every money field and authorization nonce before returning them.
|
|
69
|
+
|
|
3
70
|
## 0.4.0
|
|
4
71
|
|
|
5
72
|
- Add an opt-in persistent daily buyer limit with UTC signer/date keys, file and
|
package/README.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
Node.js 20+ SDK for discovering, quoting, and buying OpenPay x402 resources priced
|
|
4
4
|
in JPYC. It ships as plain ESM and has no build step.
|
|
5
5
|
|
|
6
|
+
Wire compatibility: x402 v1 transport (JSON 402 body with `x402Version: 1`, plus the
|
|
7
|
+
`X-PAYMENT` / `X-PAYMENT-RESPONSE` headers) with the OpenPay `extra.openpay`
|
|
8
|
+
forwarder-split extension. OpenPay's first-party resources also accept the v2 header
|
|
9
|
+
transport; this SDK speaks v1.
|
|
10
|
+
|
|
6
11
|
## Quick start
|
|
7
12
|
|
|
8
13
|
```bash
|
|
@@ -17,6 +22,7 @@ const client = createOpenPayClient({
|
|
|
17
22
|
maxPerCallJpyc: '10',
|
|
18
23
|
maxSessionJpyc: '100',
|
|
19
24
|
maxDailyJpyc: '250',
|
|
25
|
+
maxTimeoutSeconds: 600,
|
|
20
26
|
allowedHosts: 'open-pay.jp',
|
|
21
27
|
});
|
|
22
28
|
|
|
@@ -35,6 +41,12 @@ concurrent calls so every call sees the latest session total.
|
|
|
35
41
|
|
|
36
42
|
## Sell with the SDK
|
|
37
43
|
|
|
44
|
+
**An AI model is never the security boundary for payment state.** Do not unlock on any
|
|
45
|
+
model- or tool-produced claim of payment — a "paid" string, a success message, or a
|
|
46
|
+
transaction id appearing in text. The gate unlocks only on the facilitator's `verify` and
|
|
47
|
+
`settle` responses, which are backed by on-chain settlement; replayed or already-used
|
|
48
|
+
authorizations are refused at that layer.
|
|
49
|
+
|
|
38
50
|
Create a gate with the exact resource URL registered in OpenPay discovery. For
|
|
39
51
|
inexpensive content, `handle()` verifies and settles the payment in one call:
|
|
40
52
|
|
|
@@ -43,6 +55,7 @@ import { createJpycGate } from 'openpay-x402-sdk';
|
|
|
43
55
|
|
|
44
56
|
const gate = createJpycGate({
|
|
45
57
|
resourceUrl: process.env.MY_RESOURCE_URL,
|
|
58
|
+
maxUpstreamSeconds: 60,
|
|
46
59
|
});
|
|
47
60
|
|
|
48
61
|
export async function GET(request) {
|
|
@@ -91,45 +104,147 @@ minutes. Until `resourceUrl` is listed with a non-empty `accepts`, `handle()` an
|
|
|
91
104
|
`verify()` throw; map that bootstrap condition to an HTTP 500 response. Pass
|
|
92
105
|
`openpayOrigin` to use an origin other than `https://open-pay.jp`.
|
|
93
106
|
|
|
107
|
+
Before `verify()` contacts the facilitator, each gate instance claims a canonical
|
|
108
|
+
authorization identity until its `validBefore` time. Set `maxUpstreamSeconds`
|
|
109
|
+
(default `60`) to the seller's worst-case upstream duration; the gate also
|
|
110
|
+
retains `settlementGraceSeconds` (default `30`) and rejects a duplicate before
|
|
111
|
+
upstream work begins. A failed facilitator verification releases the tentative
|
|
112
|
+
claim, while a successful verification keeps it through settlement so an
|
|
113
|
+
unknown settlement result cannot expose the same authorization twice.
|
|
114
|
+
|
|
115
|
+
The facilitator reservation is an optional additional defense. The gate forwards
|
|
116
|
+
its token when one is returned and continues on the established token-less wire
|
|
117
|
+
when it is not. The local claim covers concurrent requests sharing one gate
|
|
118
|
+
instance; separate processes or serverless isolates do not share its in-memory
|
|
119
|
+
ledger.
|
|
120
|
+
|
|
94
121
|
The copy-paste paywall snippet generated by OpenPay provides the same one-shot
|
|
95
122
|
gate; `createJpycGate` is its importable SDK counterpart with split settlement.
|
|
96
123
|
|
|
124
|
+
### Dual-rail: also sell in USDC (Base) and appear on the x402 Bazaar
|
|
125
|
+
|
|
126
|
+
If your listing has the USDC face enabled, use `createDualGate` with the listing
|
|
127
|
+
id (shown as `MY_RESOURCE_ID` in the generated snippet). The 402 then carries
|
|
128
|
+
both JPYC and USDC `accepts` plus a `PAYMENT-REQUIRED` header; USDC payments are
|
|
129
|
+
relayed by OpenPay to the CDP facilitator and settle directly to your Base
|
|
130
|
+
address with 0% OpenPay fee. If the USDC face cannot be fetched (relay off or
|
|
131
|
+
unavailable), the gate degrades to JPYC-only — USDC never blocks JPYC payments.
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
import { createDualGate } from 'openpay-x402-sdk';
|
|
135
|
+
|
|
136
|
+
const gate = createDualGate({
|
|
137
|
+
resourceUrl: process.env.MY_RESOURCE_URL,
|
|
138
|
+
resourceId: process.env.MY_RESOURCE_ID,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`handle()` / `verify()` work exactly like `createJpycGate`, on both rails.
|
|
143
|
+
|
|
144
|
+
### Register listings without the web form
|
|
145
|
+
|
|
146
|
+
`createListingClient` signs in with SIWE and manages your marketplace listings
|
|
147
|
+
programmatically — so a seller (or an agent) can go from nothing to a dual-rail
|
|
148
|
+
listing entirely in code:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { createListingClient } from 'openpay-x402-sdk';
|
|
152
|
+
|
|
153
|
+
const listings = createListingClient({ privateKey: process.env.SELLER_PRIVATE_KEY });
|
|
154
|
+
const { resource, paywallSnippet } = await listings.register({
|
|
155
|
+
url: 'https://api.example.com/paid/report',
|
|
156
|
+
description: 'What the purchase completes, in one paragraph.',
|
|
157
|
+
priceJpyc: '100',
|
|
158
|
+
category: 'api',
|
|
159
|
+
usdc: { priceUsd: '0.01', serviceName: 'Example Report API' }, // optional USDC face
|
|
160
|
+
attested: true, // your personal attestation — the SDK never sets this for you
|
|
161
|
+
});
|
|
162
|
+
// resource.id → pass to createDualGate; paywallSnippet → or paste the snippet instead
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`register` refuses to run without an explicit `attested: true`: you must
|
|
166
|
+
personally affirm that you have the right to provide and charge for the
|
|
167
|
+
resource and that it is payment-gated (HTTP 402). `list()`, `update(id, input)`,
|
|
168
|
+
and `deactivate(id)` complete the lifecycle; `update` without `usdc` removes the
|
|
169
|
+
USDC face, so pass the previous value to keep it. The private key signs locally
|
|
170
|
+
and is never transmitted.
|
|
171
|
+
|
|
97
172
|
## Money guards
|
|
98
173
|
|
|
99
174
|
| Option | Default | Guard |
|
|
100
175
|
|---|---:|---|
|
|
101
176
|
| `maxPerCallJpyc` | `10` | Upper bound for the caller-provided `maxTotalJpyc`. |
|
|
102
|
-
| `maxSessionJpyc` | `100` | Cumulative cap for successful payments
|
|
177
|
+
| `maxSessionJpyc` | `100` | Cumulative cap for successful payments plus authorizations exposed to a seller. |
|
|
103
178
|
| `maxDailyJpyc` | Not set | Persistent cumulative cap per signer and UTC calendar date. |
|
|
179
|
+
| `maxTimeoutSeconds` | `600` | Reject seller-declared authorization lifetimes above this many seconds (maximum configurable value: `1200`). |
|
|
104
180
|
| `allowedHosts` | `open-pay.jp` | Comma-separated bare host allowlist. |
|
|
105
|
-
| `catalogTrust` | `true` | Also allows catalog URLs after the live challenge matches the catalog challenge. |
|
|
181
|
+
| `catalogTrust` | `true` | Also allows exact catalog URLs after the live challenge matches the catalog challenge. |
|
|
106
182
|
| `discoveryUrl` | `https://open-pay.jp/api/discovery` | Catalog and OpenPay origin used by the client. |
|
|
107
183
|
|
|
108
|
-
|
|
109
|
-
|
|
184
|
+
Catalog admission is exact URL only, including the query string. A query
|
|
185
|
+
variant must have its own reviewed listing or an explicitly allowlisted host.
|
|
110
186
|
|
|
111
187
|
`pay(url, { maxTotalJpyc })` always requires `maxTotalJpyc`. It is the maximum
|
|
112
188
|
total—including the resource price and x402 fee—that this individual call is
|
|
113
189
|
authorized to pay. It does not disable or raise `maxPerCallJpyc` or
|
|
114
190
|
`maxSessionJpyc`; every configured limit must allow the payment.
|
|
115
191
|
|
|
116
|
-
`maxDailyJpyc` is opt-in. When set, the client
|
|
117
|
-
`~/.openpay-x402/spend.json
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
192
|
+
`maxDailyJpyc` is opt-in. When set, the client atomically reserves the amount in
|
|
193
|
+
`~/.openpay-x402/spend.json` immediately before sending a signed authorization,
|
|
194
|
+
keyed by the lower-cased signer address and UTC date. A non-2xx response,
|
|
195
|
+
connection loss, or timeout does not release that reservation: the seller may
|
|
196
|
+
already have settled it. Authorizations that could remain valid across UTC
|
|
197
|
+
midnight are refused instead of being charged to the wrong day.
|
|
198
|
+
|
|
199
|
+
A missing entry starts at zero. A corrupt/unreadable store, a failed reservation,
|
|
200
|
+
or a custom store returning `null` rejects payments with
|
|
201
|
+
`daily_spend_unavailable` (fail-closed). The built-in file store uses an
|
|
202
|
+
exclusive lock plus atomic replacement, so separate processes sharing a signer
|
|
203
|
+
cannot both reserve the same remaining daily capacity. Custom stores can add an
|
|
204
|
+
atomic `reserve(key, amount, limit, reservation)` method; legacy
|
|
205
|
+
`{ load, save }` stores remain accepted with a verified pre-send write.
|
|
206
|
+
`MAX_DAILY_JPYC` is the equivalent optional setting for the exported environment
|
|
207
|
+
config readers.
|
|
208
|
+
|
|
209
|
+
An abrupt process or machine stop can leave `spend.json.lock`; reservations then
|
|
210
|
+
fail closed instead of paying without a limit. The lock file records the owning
|
|
211
|
+
`pid` and `createdAt`, and a lock whose last modification is older than 60
|
|
212
|
+
seconds is taken over automatically on the next reservation, so a killed process
|
|
213
|
+
no longer blocks budgeted payments forever. A lock younger than that is left
|
|
214
|
+
alone and the rejection names the lock path in its `detail`. Stop every process
|
|
215
|
+
that uses the same store before removing a lock by hand.
|
|
129
216
|
|
|
130
217
|
The client also rejects non-JPYC metadata, unsupported networks or schemes,
|
|
131
|
-
non-
|
|
132
|
-
|
|
218
|
+
non-canonical JPYC contracts or EIP-712 domains, seller timeouts above
|
|
219
|
+
`maxTimeoutSeconds`, unreviewed forwarder destinations, non-OpenPay forwarder
|
|
220
|
+
splits, amount inconsistencies,
|
|
221
|
+
resource URL mismatches, and catalog bait-and-switches before requesting a
|
|
222
|
+
signature. `MAX_TIMEOUT_SECONDS` is the equivalent setting for the exported
|
|
223
|
+
environment config readers. Target host/catalog admission and private-address
|
|
224
|
+
checks run before buyer target requests. Those requests require HTTPS, do not
|
|
225
|
+
follow redirects, and have a 15-second timeout. The default Node transport also
|
|
226
|
+
validates DNS before and during connection to block rebinding. A custom
|
|
227
|
+
`fetchImpl` still gets the pre-connection resolution check — a hostname that
|
|
228
|
+
resolves to a private or link-local address is rejected before the injected
|
|
229
|
+
transport is called — but connection-time rebinding protection requires also
|
|
230
|
+
supplying `lookup`, because only the transport that opens the socket can
|
|
231
|
+
re-validate the address it actually connects to. A custom `fetchImpl` without
|
|
232
|
+
`lookup` therefore remains a trusted transport boundary for connect-time
|
|
233
|
+
enforcement.
|
|
234
|
+
|
|
235
|
+
`pay()` returns a non-null `receipt` only when the facilitator signer advertised
|
|
236
|
+
by `/api/facilitator/supported` signed it and the transaction, payer, network,
|
|
237
|
+
asset, merchant amount, fee, chain, and authorization nonce all match this
|
|
238
|
+
payment. A missing, malformed, forged, or mismatched seller response header
|
|
239
|
+
becomes `receipt: null` without discarding the unlocked response body.
|
|
240
|
+
|
|
241
|
+
`pay()` also returns an explicit `settlement` field, because an HTTP `200` only
|
|
242
|
+
means the seller returned a body and is not evidence that the payment settled.
|
|
243
|
+
`verified` means a receipt header was present and its facilitator signature was
|
|
244
|
+
bound to this payment; `unverified` means a header was present but unsigned,
|
|
245
|
+
malformed, forged, or mismatched; `receipt_unavailable` means no header was
|
|
246
|
+
returned or the facilitator signer could not be resolved. Treat `unverified` and
|
|
247
|
+
`receipt_unavailable` as not proven paid.
|
|
133
248
|
|
|
134
249
|
## Signers
|
|
135
250
|
|
package/index.d.ts
CHANGED
|
@@ -44,10 +44,48 @@ export interface SpendStore {
|
|
|
44
44
|
* so an absent entry must be reported as `'0'`, never `null`.
|
|
45
45
|
*/
|
|
46
46
|
load(key: string): Promise<string | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Atomically check the limit and reserve an authorization before it is sent.
|
|
49
|
+
* Custom legacy stores may omit this method; new cross-process stores should
|
|
50
|
+
* implement it to avoid lost updates.
|
|
51
|
+
*/
|
|
52
|
+
reserve?(
|
|
53
|
+
key: string,
|
|
54
|
+
amountAtomic: string,
|
|
55
|
+
limitAtomic: string,
|
|
56
|
+
reservation: SpendReservation,
|
|
57
|
+
): Promise<SpendReservationResult>;
|
|
58
|
+
/** Mark reservation metadata confirmed without reducing the reserved total. */
|
|
59
|
+
confirm?(id: string): Promise<boolean>;
|
|
47
60
|
/** Persist the new cumulative atomic amount. Failures must not throw. */
|
|
48
61
|
save(key: string, atomicString: string): Promise<void>;
|
|
49
62
|
}
|
|
50
63
|
|
|
64
|
+
export interface SpendReservation {
|
|
65
|
+
id: string;
|
|
66
|
+
payer: Address;
|
|
67
|
+
network: string;
|
|
68
|
+
asset: Address;
|
|
69
|
+
validBefore: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type SpendReservationResult =
|
|
73
|
+
| { ok: true; totalAtomic: string }
|
|
74
|
+
| {
|
|
75
|
+
ok: false;
|
|
76
|
+
reason: 'limit_exceeded' | 'unavailable';
|
|
77
|
+
totalAtomic?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Operator-facing hint for `unavailable`, present only when the block came from the
|
|
80
|
+
* lock file (it names the lock path). Never parse it; it is diagnostic text.
|
|
81
|
+
*/
|
|
82
|
+
detail?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type PaymentLookup = (
|
|
86
|
+
hostname: string,
|
|
87
|
+
) => Promise<Array<{ address: string; family?: number }>>;
|
|
88
|
+
|
|
51
89
|
export interface FileSpendStoreOptions {
|
|
52
90
|
path?: string;
|
|
53
91
|
fsImpl?: {
|
|
@@ -58,18 +96,39 @@ export interface FileSpendStoreOptions {
|
|
|
58
96
|
data: string,
|
|
59
97
|
encoding: 'utf8',
|
|
60
98
|
): Promise<unknown>;
|
|
99
|
+
open?(
|
|
100
|
+
path: string,
|
|
101
|
+
flags: 'wx',
|
|
102
|
+
): Promise<{ close(): Promise<unknown> }>;
|
|
103
|
+
rename?(from: string, to: string): Promise<unknown>;
|
|
104
|
+
unlink?(path: string): Promise<unknown>;
|
|
105
|
+
/**
|
|
106
|
+
* Required (together with `rename`) for taking over a lock left behind by a killed
|
|
107
|
+
* process. When either is missing the takeover is disabled and the SDK warns once —
|
|
108
|
+
* a stale lock then blocks budgeted payments until it is removed by hand.
|
|
109
|
+
*/
|
|
110
|
+
stat?(path: string): Promise<{ mtimeMs?: number; mtime?: Date | number }>;
|
|
61
111
|
};
|
|
62
112
|
}
|
|
63
113
|
|
|
114
|
+
/**
|
|
115
|
+
* A spend lock older than this cannot belong to a live holder, so it may be taken over.
|
|
116
|
+
* Exported so operators and tests can reason about the same window as the store.
|
|
117
|
+
*/
|
|
118
|
+
export const SPEND_LOCK_STALE_MS: number;
|
|
119
|
+
|
|
64
120
|
interface ClientCommonOptions {
|
|
65
121
|
maxPerCallJpyc?: JpycAmount;
|
|
66
122
|
maxSessionJpyc?: JpycAmount;
|
|
67
123
|
maxDailyJpyc?: JpycAmount;
|
|
124
|
+
maxTimeoutSeconds?: number;
|
|
68
125
|
spendStore?: SpendStore;
|
|
69
126
|
allowedHosts?: string;
|
|
70
127
|
catalogTrust?: boolean;
|
|
71
128
|
discoveryUrl?: string;
|
|
72
129
|
fetchImpl?: typeof globalThis.fetch;
|
|
130
|
+
lookup?: PaymentLookup;
|
|
131
|
+
requestTimeoutMs?: number;
|
|
73
132
|
nowSec?: () => number;
|
|
74
133
|
now?: () => Date | number;
|
|
75
134
|
}
|
|
@@ -114,6 +173,11 @@ export interface RuntimeConfig {
|
|
|
114
173
|
maxPerCallAtomic: bigint;
|
|
115
174
|
maxSessionAtomic: bigint;
|
|
116
175
|
maxDailyAtomic: bigint | null;
|
|
176
|
+
/**
|
|
177
|
+
* Maximum seller-declared authorization lifetime. Optional here so existing
|
|
178
|
+
* consumers that construct RuntimeConfig manually remain source-compatible.
|
|
179
|
+
*/
|
|
180
|
+
maxTimeoutSeconds?: number;
|
|
117
181
|
allowedHosts: string[];
|
|
118
182
|
catalogTrust: boolean;
|
|
119
183
|
discoveryUrl: string;
|
|
@@ -196,10 +260,30 @@ export interface InvalidChallengeQuote {
|
|
|
196
260
|
|
|
197
261
|
export type QuoteResult = GuardedQuote | InvalidChallengeQuote;
|
|
198
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Settlement truth for one paid call. `status: 200` alone is not evidence of settlement.
|
|
265
|
+
* - `verified`: a receipt header was present and the facilitator signature bound it to this payment.
|
|
266
|
+
* - `unverified`: a receipt header was present but unsigned, malformed, forged, or mismatched.
|
|
267
|
+
* - `receipt_unavailable`: no receipt header, or the facilitator signer could not be resolved.
|
|
268
|
+
*/
|
|
269
|
+
export type SettlementStatus = 'verified' | 'unverified' | 'receipt_unavailable';
|
|
270
|
+
|
|
271
|
+
/** The `SettlementStatus` values as a runtime object (`unavailable` = `receipt_unavailable`). */
|
|
272
|
+
export const SETTLEMENT: {
|
|
273
|
+
readonly verified: 'verified';
|
|
274
|
+
readonly unverified: 'unverified';
|
|
275
|
+
readonly unavailable: 'receipt_unavailable';
|
|
276
|
+
};
|
|
277
|
+
|
|
199
278
|
export interface PaymentResult {
|
|
200
279
|
status: number;
|
|
201
280
|
body: unknown;
|
|
202
281
|
receipt: unknown;
|
|
282
|
+
/**
|
|
283
|
+
* Optional so objects built before this field existed stay source-compatible. Absent must be
|
|
284
|
+
* read the same way as `receipt_unavailable`: no settlement was proven.
|
|
285
|
+
*/
|
|
286
|
+
settlement?: SettlementStatus;
|
|
203
287
|
}
|
|
204
288
|
|
|
205
289
|
export interface OpenPayClient {
|
|
@@ -228,6 +312,10 @@ export interface JpycGateOptions {
|
|
|
228
312
|
openpayOrigin?: string;
|
|
229
313
|
fetchImpl?: typeof globalThis.fetch;
|
|
230
314
|
now?: () => number;
|
|
315
|
+
/** Worst-case seller upstream duration protected by the local claim. Default: 60. */
|
|
316
|
+
maxUpstreamSeconds?: number;
|
|
317
|
+
/** Extra validity retained for settlement after upstream work. Default: 30. */
|
|
318
|
+
settlementGraceSeconds?: number;
|
|
231
319
|
}
|
|
232
320
|
|
|
233
321
|
export interface JpycGatePaymentResponse {
|
|
@@ -245,9 +333,98 @@ export interface JpycGate {
|
|
|
245
333
|
|
|
246
334
|
export function createJpycGate(options: JpycGateOptions): JpycGate;
|
|
247
335
|
|
|
336
|
+
export interface DualGateOptions extends JpycGateOptions {
|
|
337
|
+
/** OpenPay listing id (MY_RESOURCE_ID in the generated snippet). Enables the USDC (Base) rail. */
|
|
338
|
+
resourceId: string;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Dual-rail seller gate: JPYC (Polygon, OpenPay facilitator) plus USDC (Base, standard x402
|
|
343
|
+
* relayed to the CDP facilitator via OpenPay). If the USDC face cannot be fetched (relay off
|
|
344
|
+
* or unavailable), the gate degrades to JPYC-only — the USDC side never blocks JPYC payments.
|
|
345
|
+
*/
|
|
346
|
+
export function createDualGate(options: DualGateOptions): JpycGate;
|
|
347
|
+
|
|
348
|
+
export interface ListingUsdcInput {
|
|
349
|
+
/** USD price without a $ sign, up to 6 decimals (e.g. "0.005"). */
|
|
350
|
+
priceUsd: string;
|
|
351
|
+
/** USDC (Base) receiving address. Defaults to the JPYC payTo. */
|
|
352
|
+
payTo?: string;
|
|
353
|
+
/** Display name for x402 Bazaar search (max 60 chars). */
|
|
354
|
+
serviceName?: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface ListingInput {
|
|
358
|
+
url: string;
|
|
359
|
+
description: string;
|
|
360
|
+
/** Integer JPYC price as a string (e.g. "100"). */
|
|
361
|
+
priceJpyc: string;
|
|
362
|
+
category: string;
|
|
363
|
+
docsUrl?: string;
|
|
364
|
+
license?: string;
|
|
365
|
+
/** JPYC receiving address. Defaults to the signed-in wallet. */
|
|
366
|
+
payTo?: string;
|
|
367
|
+
/** Enable the USDC (Base) face — also lists on the x402 Bazaar after the first settle. */
|
|
368
|
+
usdc?: ListingUsdcInput;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export interface RegisterListingInput extends ListingInput {
|
|
372
|
+
/**
|
|
373
|
+
* Required, must be literally true: your personal attestation that you have the right to
|
|
374
|
+
* provide and charge for this resource and that it is payment-gated (HTTP 402).
|
|
375
|
+
* The SDK never sets this for you.
|
|
376
|
+
*/
|
|
377
|
+
attested: true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface ListingRecord {
|
|
381
|
+
id: string;
|
|
382
|
+
url: string;
|
|
383
|
+
description: string;
|
|
384
|
+
priceJpyc: string;
|
|
385
|
+
category: string;
|
|
386
|
+
payTo: string;
|
|
387
|
+
docsUrl?: string;
|
|
388
|
+
license?: string;
|
|
389
|
+
usdc?: { payTo: string; priceUsd: string; serviceName?: string };
|
|
390
|
+
paywallSnippet?: string;
|
|
391
|
+
hidden?: boolean;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export interface RegisterListingResult {
|
|
395
|
+
resource: ListingRecord;
|
|
396
|
+
/** Copy-paste 402 gate for your server (dual-rail x402Gate when usdc is set). */
|
|
397
|
+
paywallSnippet: string;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export interface ListingClient {
|
|
401
|
+
/** The seller wallet address that signs in via SIWE (checksummed). */
|
|
402
|
+
address: Address;
|
|
403
|
+
register(input: RegisterListingInput): Promise<RegisterListingResult>;
|
|
404
|
+
list(): Promise<ListingRecord[]>;
|
|
405
|
+
update(id: string, input: ListingInput): Promise<{ resource: ListingRecord }>;
|
|
406
|
+
deactivate(id: string): Promise<boolean>;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Programmatic listing client — register, list, update, and deactivate OpenPay marketplace
|
|
411
|
+
* listings without the web form. Signs in with SIWE using the given private key on first use;
|
|
412
|
+
* the key is only used to sign locally and is never transmitted.
|
|
413
|
+
*/
|
|
414
|
+
export function createListingClient(options: {
|
|
415
|
+
privateKey: string;
|
|
416
|
+
openpayOrigin?: string;
|
|
417
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
418
|
+
/** SIWE chainId (default 137 = Polygon). */
|
|
419
|
+
chainId?: number;
|
|
420
|
+
statement?: string;
|
|
421
|
+
now?: () => number;
|
|
422
|
+
}): ListingClient;
|
|
423
|
+
|
|
248
424
|
export const RECEIVE_WITH_AUTHORIZATION_TYPES: {
|
|
249
425
|
ReceiveWithAuthorization: Array<{ name: string; type: string }>;
|
|
250
426
|
};
|
|
427
|
+
export const MAX_AUTHORIZATION_TIMEOUT_SECONDS: 1200;
|
|
251
428
|
|
|
252
429
|
export interface NormalizedPaymentRequirements {
|
|
253
430
|
scheme: 'exact';
|
|
@@ -331,6 +508,8 @@ export function paymentPayloadFor(
|
|
|
331
508
|
export const JPYC_DECIMALS: 18;
|
|
332
509
|
export const DEFAULT_MAX_PER_CALL_JPYC: '10';
|
|
333
510
|
export const DEFAULT_MAX_SESSION_JPYC: '100';
|
|
511
|
+
export const DEFAULT_MAX_TIMEOUT_SECONDS: 600;
|
|
512
|
+
export const MAX_SUPPORTED_TIMEOUT_SECONDS: 1200;
|
|
334
513
|
export const DEFAULT_ALLOWED_HOSTS: 'open-pay.jp';
|
|
335
514
|
export const DEFAULT_CATALOG_TRUST: true;
|
|
336
515
|
export const DEFAULT_DISCOVERY_URL: 'https://open-pay.jp/api/discovery';
|
|
@@ -340,8 +519,10 @@ export const REASONS: {
|
|
|
340
519
|
unsupportedScheme: 'unsupported_scheme';
|
|
341
520
|
unsupportedNetwork: 'unsupported_network';
|
|
342
521
|
invalidOpenpayMode: 'invalid_openpay_mode';
|
|
522
|
+
invalidOpenpayForwarder: 'invalid_openpay_forwarder';
|
|
343
523
|
amountMismatch: 'amount_mismatch';
|
|
344
524
|
invalidJpycAsset: 'invalid_jpyc_asset';
|
|
525
|
+
timeoutTooLong: 'timeout_too_long';
|
|
345
526
|
resourceMismatch: 'resource_mismatch';
|
|
346
527
|
invalidAccept: 'invalid_accept';
|
|
347
528
|
maxTotalRequired: 'max_total_required';
|
|
@@ -352,12 +533,62 @@ export const REASONS: {
|
|
|
352
533
|
sessionLimitExceeded: 'session_limit_exceeded';
|
|
353
534
|
dailyLimitExceeded: 'daily_limit_exceeded';
|
|
354
535
|
dailySpendUnavailable: 'daily_spend_unavailable';
|
|
536
|
+
dailyAuthorizationCrossesUtcDay: 'daily_authorization_crosses_utc_day';
|
|
355
537
|
buyerPrivateKeyMissing: 'buyer_private_key_missing';
|
|
356
538
|
stewardSignerUnconfigured: 'steward_signer_unconfigured';
|
|
357
539
|
catalogAcceptMismatch: 'catalog_accept_mismatch';
|
|
358
540
|
};
|
|
541
|
+
export const SUPPORTED_JPYC_ASSETS: Readonly<
|
|
542
|
+
Record<
|
|
543
|
+
string,
|
|
544
|
+
Readonly<{
|
|
545
|
+
address: Address;
|
|
546
|
+
name: 'JPY Coin';
|
|
547
|
+
version: '1';
|
|
548
|
+
decimals: 18;
|
|
549
|
+
}>
|
|
550
|
+
>
|
|
551
|
+
>;
|
|
552
|
+
export const SUPPORTED_JPYC_FORWARDERS: Readonly<
|
|
553
|
+
Partial<Record<string, Address>>
|
|
554
|
+
>;
|
|
359
555
|
export const SUPPORTED_NETWORKS: Set<string>;
|
|
360
556
|
|
|
557
|
+
export const DEFAULT_PAYMENT_FETCH_TIMEOUT_MS: 15000;
|
|
558
|
+
export function isPrivatePaymentHost(hostname: string): boolean;
|
|
559
|
+
export function parseSafePaymentUrl(raw: unknown): URL | null;
|
|
560
|
+
export function fetchPaymentTarget(
|
|
561
|
+
url: string,
|
|
562
|
+
options?: {
|
|
563
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
564
|
+
headers?: HeadersInit;
|
|
565
|
+
lookup?: PaymentLookup;
|
|
566
|
+
timeoutMs?: number;
|
|
567
|
+
},
|
|
568
|
+
): Promise<Response>;
|
|
569
|
+
|
|
570
|
+
export type ReceiptSignerResolver = () => Promise<Address | null>;
|
|
571
|
+
export function createReceiptSignerResolver(options: {
|
|
572
|
+
discoveryUrl: string;
|
|
573
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
574
|
+
lookup?: PaymentLookup;
|
|
575
|
+
requestTimeoutMs?: number;
|
|
576
|
+
}): ReceiptSignerResolver;
|
|
577
|
+
export function verifyBoundPaymentResponse(
|
|
578
|
+
paymentResponse: unknown,
|
|
579
|
+
expected: {
|
|
580
|
+
expectedSigner: Address;
|
|
581
|
+
payer: Address;
|
|
582
|
+
network: string;
|
|
583
|
+
asset: Address;
|
|
584
|
+
chainId: number;
|
|
585
|
+
merchant: Address;
|
|
586
|
+
merchantValue: bigint;
|
|
587
|
+
feeValue: bigint;
|
|
588
|
+
nonce: Hex;
|
|
589
|
+
},
|
|
590
|
+
): Promise<boolean>;
|
|
591
|
+
|
|
361
592
|
export interface AcceptSummary {
|
|
362
593
|
priceAtomic: bigint;
|
|
363
594
|
feeAtomic: bigint;
|
|
@@ -459,12 +690,16 @@ export function createCatalogCache(): CatalogCache;
|
|
|
459
690
|
export function resolveCatalogListings(options: {
|
|
460
691
|
config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
|
|
461
692
|
fetchImpl?: typeof globalThis.fetch;
|
|
693
|
+
lookup?: PaymentLookup;
|
|
694
|
+
requestTimeoutMs?: number;
|
|
462
695
|
now?: () => number;
|
|
463
696
|
cache?: CatalogCache;
|
|
464
697
|
}): Promise<Map<string, unknown> | null>;
|
|
465
698
|
export function createCatalogResolver(options: {
|
|
466
699
|
config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
|
|
467
700
|
fetchImpl?: typeof globalThis.fetch;
|
|
701
|
+
lookup?: PaymentLookup;
|
|
702
|
+
requestTimeoutMs?: number;
|
|
468
703
|
now?: () => number;
|
|
469
704
|
}): () => Promise<Map<string, unknown> | null>;
|
|
470
705
|
|
|
@@ -482,7 +717,10 @@ export function createPaymentExecutor(options: {
|
|
|
482
717
|
signerAddress?: Address | null;
|
|
483
718
|
spendStore?: SpendStore | null;
|
|
484
719
|
fetchImpl?: typeof globalThis.fetch;
|
|
720
|
+
lookup?: PaymentLookup;
|
|
721
|
+
requestTimeoutMs?: number;
|
|
485
722
|
nowSec?: () => number;
|
|
486
723
|
now?: () => Date | number;
|
|
487
724
|
resolveCatalogListings?: () => Promise<Map<string, unknown> | null>;
|
|
725
|
+
resolveReceiptSigner?: ReceiptSignerResolver;
|
|
488
726
|
}): PaymentExecutor;
|