openpay-x402-sdk 0.3.0 → 0.5.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 +26 -0
- package/README.md +74 -7
- package/index.d.ts +151 -0
- package/package.json +1 -1
- package/src/catalog.mjs +27 -3
- package/src/client.mjs +24 -1
- package/src/executor.mjs +423 -30
- package/src/gate.mjs +151 -6
- package/src/guards.mjs +132 -24
- package/src/index.mjs +3 -0
- package/src/network.mjs +254 -0
- package/src/payment.mjs +20 -1
- package/src/receipt.mjs +201 -0
- package/src/spendStore.mjs +305 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- Reserve session and daily capacity immediately before exposing a signed
|
|
6
|
+
authorization. Non-2xx responses, timeouts, and connection failures retain
|
|
7
|
+
the reservation; successful 2xx responses keep the existing confirmed-spend
|
|
8
|
+
accounting.
|
|
9
|
+
- Make the file daily store cross-process atomic with an exclusive lock, reject
|
|
10
|
+
UTC-crossing authorizations, and fail closed when a configured store is
|
|
11
|
+
unavailable.
|
|
12
|
+
- Enforce host/catalog admission before target I/O, block private and rebinding
|
|
13
|
+
destinations, require exact catalog URLs, stop redirects, and bound buyer
|
|
14
|
+
requests with a timeout.
|
|
15
|
+
- Bind supported networks to the canonical JPYC v3 contract/domain, cap
|
|
16
|
+
seller-declared authorization lifetimes, bind signature destinations to a
|
|
17
|
+
known or catalog-reviewed forwarder, and locally reserve seller-gate
|
|
18
|
+
authorizations across verify and settle without requiring facilitator tokens.
|
|
19
|
+
- Verify facilitator-signed payment receipts against the advertised signer and
|
|
20
|
+
bind every money field and authorization nonce before returning them.
|
|
21
|
+
|
|
22
|
+
## 0.4.0
|
|
23
|
+
|
|
24
|
+
- Add an opt-in persistent daily buyer limit with UTC signer/date keys, file and
|
|
25
|
+
injectable spend stores, quote-time visibility, and fail-closed reads.
|
|
26
|
+
- Record daily spend only after successful 2xx unlocks while isolating store
|
|
27
|
+
write failures from already completed payment responses.
|
|
28
|
+
|
|
3
29
|
## 0.3.0
|
|
4
30
|
|
|
5
31
|
- Add `createJpycGate` for seller-side x402 gates backed by the OpenPay catalog,
|
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
|
|
@@ -16,6 +21,8 @@ const client = createOpenPayClient({
|
|
|
16
21
|
privateKey: process.env.BUYER_PRIVATE_KEY,
|
|
17
22
|
maxPerCallJpyc: '10',
|
|
18
23
|
maxSessionJpyc: '100',
|
|
24
|
+
maxDailyJpyc: '250',
|
|
25
|
+
maxTimeoutSeconds: 600,
|
|
19
26
|
allowedHosts: 'open-pay.jp',
|
|
20
27
|
});
|
|
21
28
|
|
|
@@ -34,6 +41,12 @@ concurrent calls so every call sees the latest session total.
|
|
|
34
41
|
|
|
35
42
|
## Sell with the SDK
|
|
36
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
|
+
|
|
37
50
|
Create a gate with the exact resource URL registered in OpenPay discovery. For
|
|
38
51
|
inexpensive content, `handle()` verifies and settles the payment in one call:
|
|
39
52
|
|
|
@@ -42,6 +55,7 @@ import { createJpycGate } from 'openpay-x402-sdk';
|
|
|
42
55
|
|
|
43
56
|
const gate = createJpycGate({
|
|
44
57
|
resourceUrl: process.env.MY_RESOURCE_URL,
|
|
58
|
+
maxUpstreamSeconds: 60,
|
|
45
59
|
});
|
|
46
60
|
|
|
47
61
|
export async function GET(request) {
|
|
@@ -90,6 +104,20 @@ minutes. Until `resourceUrl` is listed with a non-empty `accepts`, `handle()` an
|
|
|
90
104
|
`verify()` throw; map that bootstrap condition to an HTTP 500 response. Pass
|
|
91
105
|
`openpayOrigin` to use an origin other than `https://open-pay.jp`.
|
|
92
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
|
+
|
|
93
121
|
The copy-paste paywall snippet generated by OpenPay provides the same one-shot
|
|
94
122
|
gate; `createJpycGate` is its importable SDK counterpart with split settlement.
|
|
95
123
|
|
|
@@ -98,22 +126,61 @@ gate; `createJpycGate` is its importable SDK counterpart with split settlement.
|
|
|
98
126
|
| Option | Default | Guard |
|
|
99
127
|
|---|---:|---|
|
|
100
128
|
| `maxPerCallJpyc` | `10` | Upper bound for the caller-provided `maxTotalJpyc`. |
|
|
101
|
-
| `maxSessionJpyc` | `100` | Cumulative cap for successful payments
|
|
129
|
+
| `maxSessionJpyc` | `100` | Cumulative cap for successful payments plus authorizations exposed to a seller. |
|
|
130
|
+
| `maxDailyJpyc` | Not set | Persistent cumulative cap per signer and UTC calendar date. |
|
|
131
|
+
| `maxTimeoutSeconds` | `600` | Reject seller-declared authorization lifetimes above this many seconds (maximum configurable value: `1200`). |
|
|
102
132
|
| `allowedHosts` | `open-pay.jp` | Comma-separated bare host allowlist. |
|
|
103
|
-
| `catalogTrust` | `true` | Also allows catalog URLs after the live challenge matches the catalog challenge. |
|
|
133
|
+
| `catalogTrust` | `true` | Also allows exact catalog URLs after the live challenge matches the catalog challenge. |
|
|
104
134
|
| `discoveryUrl` | `https://open-pay.jp/api/discovery` | Catalog and OpenPay origin used by the client. |
|
|
105
135
|
|
|
106
|
-
|
|
107
|
-
|
|
136
|
+
Catalog admission is exact URL only, including the query string. A query
|
|
137
|
+
variant must have its own reviewed listing or an explicitly allowlisted host.
|
|
108
138
|
|
|
109
139
|
`pay(url, { maxTotalJpyc })` always requires `maxTotalJpyc`. It is the maximum
|
|
110
140
|
total—including the resource price and x402 fee—that this individual call is
|
|
111
141
|
authorized to pay. It does not disable or raise `maxPerCallJpyc` or
|
|
112
|
-
`maxSessionJpyc`;
|
|
142
|
+
`maxSessionJpyc`; every configured limit must allow the payment.
|
|
143
|
+
|
|
144
|
+
`maxDailyJpyc` is opt-in. When set, the client atomically reserves the amount in
|
|
145
|
+
`~/.openpay-x402/spend.json` immediately before sending a signed authorization,
|
|
146
|
+
keyed by the lower-cased signer address and UTC date. A non-2xx response,
|
|
147
|
+
connection loss, or timeout does not release that reservation: the seller may
|
|
148
|
+
already have settled it. Authorizations that could remain valid across UTC
|
|
149
|
+
midnight are refused instead of being charged to the wrong day.
|
|
150
|
+
|
|
151
|
+
A missing entry starts at zero. A corrupt/unreadable store, a failed reservation,
|
|
152
|
+
or a custom store returning `null` rejects payments with
|
|
153
|
+
`daily_spend_unavailable` (fail-closed). The built-in file store uses an
|
|
154
|
+
exclusive lock plus atomic replacement, so separate processes sharing a signer
|
|
155
|
+
cannot both reserve the same remaining daily capacity. Custom stores can add an
|
|
156
|
+
atomic `reserve(key, amount, limit, reservation)` method; legacy
|
|
157
|
+
`{ load, save }` stores remain accepted with a verified pre-send write.
|
|
158
|
+
`MAX_DAILY_JPYC` is the equivalent optional setting for the exported environment
|
|
159
|
+
config readers.
|
|
160
|
+
|
|
161
|
+
An abrupt process or machine stop can leave `spend.json.lock`; reservations then
|
|
162
|
+
fail closed instead of paying without a limit. Stop every process that uses the
|
|
163
|
+
same store before inspecting and manually removing a stale lock. Never remove
|
|
164
|
+
the lock while another process may still own it.
|
|
113
165
|
|
|
114
166
|
The client also rejects non-JPYC metadata, unsupported networks or schemes,
|
|
115
|
-
non-
|
|
116
|
-
|
|
167
|
+
non-canonical JPYC contracts or EIP-712 domains, seller timeouts above
|
|
168
|
+
`maxTimeoutSeconds`, unreviewed forwarder destinations, non-OpenPay forwarder
|
|
169
|
+
splits, amount inconsistencies,
|
|
170
|
+
resource URL mismatches, and catalog bait-and-switches before requesting a
|
|
171
|
+
signature. `MAX_TIMEOUT_SECONDS` is the equivalent setting for the exported
|
|
172
|
+
environment config readers. Target host/catalog admission and private-address
|
|
173
|
+
checks run before buyer target requests. Those requests require HTTPS, do not
|
|
174
|
+
follow redirects, and have a 15-second timeout. The default Node transport also
|
|
175
|
+
validates DNS before and during connection to block rebinding; an explicitly
|
|
176
|
+
injected custom `fetchImpl` is a trusted transport boundary and is responsible
|
|
177
|
+
for equivalent connect-time enforcement.
|
|
178
|
+
|
|
179
|
+
`pay()` returns a non-null `receipt` only when the facilitator signer advertised
|
|
180
|
+
by `/api/facilitator/supported` signed it and the transaction, payer, network,
|
|
181
|
+
asset, merchant amount, fee, chain, and authorization nonce all match this
|
|
182
|
+
payment. A missing, malformed, forged, or mismatched seller response header
|
|
183
|
+
becomes `receipt: null` without discarding the unlocked response body.
|
|
117
184
|
|
|
118
185
|
## Signers
|
|
119
186
|
|
package/index.d.ts
CHANGED
|
@@ -37,13 +37,83 @@ export interface PaymentSigner {
|
|
|
37
37
|
signTypedData(typedData: PaymentTypedData): Hex | Promise<Hex>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export interface SpendStore {
|
|
41
|
+
/**
|
|
42
|
+
* Return the atomic JPYC spent for the key, `'0'` when no record exists, or
|
|
43
|
+
* `null` only on read failure. `null` fails closed (payments are refused),
|
|
44
|
+
* so an absent entry must be reported as `'0'`, never `null`.
|
|
45
|
+
*/
|
|
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>;
|
|
60
|
+
/** Persist the new cumulative atomic amount. Failures must not throw. */
|
|
61
|
+
save(key: string, atomicString: string): Promise<void>;
|
|
62
|
+
}
|
|
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
|
+
|
|
80
|
+
export type PaymentLookup = (
|
|
81
|
+
hostname: string,
|
|
82
|
+
) => Promise<Array<{ address: string; family?: number }>>;
|
|
83
|
+
|
|
84
|
+
export interface FileSpendStoreOptions {
|
|
85
|
+
path?: string;
|
|
86
|
+
fsImpl?: {
|
|
87
|
+
readFile(path: string, encoding: 'utf8'): Promise<string>;
|
|
88
|
+
mkdir(path: string, options: { recursive: true }): Promise<unknown>;
|
|
89
|
+
writeFile(
|
|
90
|
+
path: string,
|
|
91
|
+
data: string,
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
): Promise<unknown>;
|
|
94
|
+
open?(
|
|
95
|
+
path: string,
|
|
96
|
+
flags: 'wx',
|
|
97
|
+
): Promise<{ close(): Promise<unknown> }>;
|
|
98
|
+
rename?(from: string, to: string): Promise<unknown>;
|
|
99
|
+
unlink?(path: string): Promise<unknown>;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
40
103
|
interface ClientCommonOptions {
|
|
41
104
|
maxPerCallJpyc?: JpycAmount;
|
|
42
105
|
maxSessionJpyc?: JpycAmount;
|
|
106
|
+
maxDailyJpyc?: JpycAmount;
|
|
107
|
+
maxTimeoutSeconds?: number;
|
|
108
|
+
spendStore?: SpendStore;
|
|
43
109
|
allowedHosts?: string;
|
|
44
110
|
catalogTrust?: boolean;
|
|
45
111
|
discoveryUrl?: string;
|
|
46
112
|
fetchImpl?: typeof globalThis.fetch;
|
|
113
|
+
lookup?: PaymentLookup;
|
|
114
|
+
requestTimeoutMs?: number;
|
|
115
|
+
nowSec?: () => number;
|
|
116
|
+
now?: () => Date | number;
|
|
47
117
|
}
|
|
48
118
|
|
|
49
119
|
type NoSignerOptions = {
|
|
@@ -85,6 +155,12 @@ export interface RuntimeConfig {
|
|
|
85
155
|
stewardSignerSecret: string | null;
|
|
86
156
|
maxPerCallAtomic: bigint;
|
|
87
157
|
maxSessionAtomic: bigint;
|
|
158
|
+
maxDailyAtomic: bigint | null;
|
|
159
|
+
/**
|
|
160
|
+
* Maximum seller-declared authorization lifetime. Optional here so existing
|
|
161
|
+
* consumers that construct RuntimeConfig manually remain source-compatible.
|
|
162
|
+
*/
|
|
163
|
+
maxTimeoutSeconds?: number;
|
|
88
164
|
allowedHosts: string[];
|
|
89
165
|
catalogTrust: boolean;
|
|
90
166
|
discoveryUrl: string;
|
|
@@ -199,6 +275,10 @@ export interface JpycGateOptions {
|
|
|
199
275
|
openpayOrigin?: string;
|
|
200
276
|
fetchImpl?: typeof globalThis.fetch;
|
|
201
277
|
now?: () => number;
|
|
278
|
+
/** Worst-case seller upstream duration protected by the local claim. Default: 60. */
|
|
279
|
+
maxUpstreamSeconds?: number;
|
|
280
|
+
/** Extra validity retained for settlement after upstream work. Default: 30. */
|
|
281
|
+
settlementGraceSeconds?: number;
|
|
202
282
|
}
|
|
203
283
|
|
|
204
284
|
export interface JpycGatePaymentResponse {
|
|
@@ -219,6 +299,7 @@ export function createJpycGate(options: JpycGateOptions): JpycGate;
|
|
|
219
299
|
export const RECEIVE_WITH_AUTHORIZATION_TYPES: {
|
|
220
300
|
ReceiveWithAuthorization: Array<{ name: string; type: string }>;
|
|
221
301
|
};
|
|
302
|
+
export const MAX_AUTHORIZATION_TIMEOUT_SECONDS: 1200;
|
|
222
303
|
|
|
223
304
|
export interface NormalizedPaymentRequirements {
|
|
224
305
|
scheme: 'exact';
|
|
@@ -302,6 +383,8 @@ export function paymentPayloadFor(
|
|
|
302
383
|
export const JPYC_DECIMALS: 18;
|
|
303
384
|
export const DEFAULT_MAX_PER_CALL_JPYC: '10';
|
|
304
385
|
export const DEFAULT_MAX_SESSION_JPYC: '100';
|
|
386
|
+
export const DEFAULT_MAX_TIMEOUT_SECONDS: 600;
|
|
387
|
+
export const MAX_SUPPORTED_TIMEOUT_SECONDS: 1200;
|
|
305
388
|
export const DEFAULT_ALLOWED_HOSTS: 'open-pay.jp';
|
|
306
389
|
export const DEFAULT_CATALOG_TRUST: true;
|
|
307
390
|
export const DEFAULT_DISCOVERY_URL: 'https://open-pay.jp/api/discovery';
|
|
@@ -311,8 +394,10 @@ export const REASONS: {
|
|
|
311
394
|
unsupportedScheme: 'unsupported_scheme';
|
|
312
395
|
unsupportedNetwork: 'unsupported_network';
|
|
313
396
|
invalidOpenpayMode: 'invalid_openpay_mode';
|
|
397
|
+
invalidOpenpayForwarder: 'invalid_openpay_forwarder';
|
|
314
398
|
amountMismatch: 'amount_mismatch';
|
|
315
399
|
invalidJpycAsset: 'invalid_jpyc_asset';
|
|
400
|
+
timeoutTooLong: 'timeout_too_long';
|
|
316
401
|
resourceMismatch: 'resource_mismatch';
|
|
317
402
|
invalidAccept: 'invalid_accept';
|
|
318
403
|
maxTotalRequired: 'max_total_required';
|
|
@@ -321,12 +406,64 @@ export const REASONS: {
|
|
|
321
406
|
maxTotalAbovePerCallLimit: 'max_total_above_per_call_limit';
|
|
322
407
|
perCallLimitExceeded: 'per_call_limit_exceeded';
|
|
323
408
|
sessionLimitExceeded: 'session_limit_exceeded';
|
|
409
|
+
dailyLimitExceeded: 'daily_limit_exceeded';
|
|
410
|
+
dailySpendUnavailable: 'daily_spend_unavailable';
|
|
411
|
+
dailyAuthorizationCrossesUtcDay: 'daily_authorization_crosses_utc_day';
|
|
324
412
|
buyerPrivateKeyMissing: 'buyer_private_key_missing';
|
|
325
413
|
stewardSignerUnconfigured: 'steward_signer_unconfigured';
|
|
326
414
|
catalogAcceptMismatch: 'catalog_accept_mismatch';
|
|
327
415
|
};
|
|
416
|
+
export const SUPPORTED_JPYC_ASSETS: Readonly<
|
|
417
|
+
Record<
|
|
418
|
+
string,
|
|
419
|
+
Readonly<{
|
|
420
|
+
address: Address;
|
|
421
|
+
name: 'JPY Coin';
|
|
422
|
+
version: '1';
|
|
423
|
+
decimals: 18;
|
|
424
|
+
}>
|
|
425
|
+
>
|
|
426
|
+
>;
|
|
427
|
+
export const SUPPORTED_JPYC_FORWARDERS: Readonly<
|
|
428
|
+
Partial<Record<string, Address>>
|
|
429
|
+
>;
|
|
328
430
|
export const SUPPORTED_NETWORKS: Set<string>;
|
|
329
431
|
|
|
432
|
+
export const DEFAULT_PAYMENT_FETCH_TIMEOUT_MS: 15000;
|
|
433
|
+
export function isPrivatePaymentHost(hostname: string): boolean;
|
|
434
|
+
export function parseSafePaymentUrl(raw: unknown): URL | null;
|
|
435
|
+
export function fetchPaymentTarget(
|
|
436
|
+
url: string,
|
|
437
|
+
options?: {
|
|
438
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
439
|
+
headers?: HeadersInit;
|
|
440
|
+
lookup?: PaymentLookup;
|
|
441
|
+
timeoutMs?: number;
|
|
442
|
+
},
|
|
443
|
+
): Promise<Response>;
|
|
444
|
+
|
|
445
|
+
export type ReceiptSignerResolver = () => Promise<Address | null>;
|
|
446
|
+
export function createReceiptSignerResolver(options: {
|
|
447
|
+
discoveryUrl: string;
|
|
448
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
449
|
+
lookup?: PaymentLookup;
|
|
450
|
+
requestTimeoutMs?: number;
|
|
451
|
+
}): ReceiptSignerResolver;
|
|
452
|
+
export function verifyBoundPaymentResponse(
|
|
453
|
+
paymentResponse: unknown,
|
|
454
|
+
expected: {
|
|
455
|
+
expectedSigner: Address;
|
|
456
|
+
payer: Address;
|
|
457
|
+
network: string;
|
|
458
|
+
asset: Address;
|
|
459
|
+
chainId: number;
|
|
460
|
+
merchant: Address;
|
|
461
|
+
merchantValue: bigint;
|
|
462
|
+
feeValue: bigint;
|
|
463
|
+
nonce: Hex;
|
|
464
|
+
},
|
|
465
|
+
): Promise<boolean>;
|
|
466
|
+
|
|
330
467
|
export interface AcceptSummary {
|
|
331
468
|
priceAtomic: bigint;
|
|
332
469
|
feeAtomic: bigint;
|
|
@@ -358,6 +495,9 @@ export function parseClientOptions(
|
|
|
358
495
|
options?: OpenPayClientOptions,
|
|
359
496
|
): RuntimeConfig;
|
|
360
497
|
export function createPaymentSession(initialSpentAtomic?: bigint): PaymentSession;
|
|
498
|
+
export function createFileSpendStore(
|
|
499
|
+
options?: FileSpendStoreOptions,
|
|
500
|
+
): SpendStore;
|
|
361
501
|
export function recordSuccessfulPayment(
|
|
362
502
|
session: PaymentSession,
|
|
363
503
|
amountAtomic: bigint,
|
|
@@ -375,6 +515,7 @@ export function evaluatePaymentGuards(options: {
|
|
|
375
515
|
accept: unknown;
|
|
376
516
|
config: Omit<RuntimeConfig, 'discoveryUrl'> | RuntimeConfig;
|
|
377
517
|
sessionSpentAtomic?: bigint;
|
|
518
|
+
dailySpentAtomic?: bigint | null;
|
|
378
519
|
maxTotalJpyc?: JpycAmount;
|
|
379
520
|
requireMaxTotal?: boolean;
|
|
380
521
|
requirePrivateKey?: boolean;
|
|
@@ -424,12 +565,16 @@ export function createCatalogCache(): CatalogCache;
|
|
|
424
565
|
export function resolveCatalogListings(options: {
|
|
425
566
|
config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
|
|
426
567
|
fetchImpl?: typeof globalThis.fetch;
|
|
568
|
+
lookup?: PaymentLookup;
|
|
569
|
+
requestTimeoutMs?: number;
|
|
427
570
|
now?: () => number;
|
|
428
571
|
cache?: CatalogCache;
|
|
429
572
|
}): Promise<Map<string, unknown> | null>;
|
|
430
573
|
export function createCatalogResolver(options: {
|
|
431
574
|
config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
|
|
432
575
|
fetchImpl?: typeof globalThis.fetch;
|
|
576
|
+
lookup?: PaymentLookup;
|
|
577
|
+
requestTimeoutMs?: number;
|
|
433
578
|
now?: () => number;
|
|
434
579
|
}): () => Promise<Map<string, unknown> | null>;
|
|
435
580
|
|
|
@@ -444,7 +589,13 @@ export function createPaymentExecutor(options: {
|
|
|
444
589
|
config: RuntimeConfig;
|
|
445
590
|
session: PaymentSession;
|
|
446
591
|
signer?: PaymentSigner | null;
|
|
592
|
+
signerAddress?: Address | null;
|
|
593
|
+
spendStore?: SpendStore | null;
|
|
447
594
|
fetchImpl?: typeof globalThis.fetch;
|
|
595
|
+
lookup?: PaymentLookup;
|
|
596
|
+
requestTimeoutMs?: number;
|
|
448
597
|
nowSec?: () => number;
|
|
598
|
+
now?: () => Date | number;
|
|
449
599
|
resolveCatalogListings?: () => Promise<Map<string, unknown> | null>;
|
|
600
|
+
resolveReceiptSigner?: ReceiptSignerResolver;
|
|
450
601
|
}): PaymentExecutor;
|
package/package.json
CHANGED
package/src/catalog.mjs
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PAYMENT_FETCH_TIMEOUT_MS,
|
|
3
|
+
fetchPaymentTarget,
|
|
4
|
+
} from './network.mjs';
|
|
5
|
+
|
|
1
6
|
export const CATALOG_CACHE_MS = 5 * 60_000;
|
|
2
7
|
|
|
3
8
|
function isObject(value) {
|
|
@@ -21,6 +26,8 @@ export function createCatalogCache() {
|
|
|
21
26
|
export async function resolveCatalogListings({
|
|
22
27
|
config,
|
|
23
28
|
fetchImpl = fetch,
|
|
29
|
+
lookup,
|
|
30
|
+
requestTimeoutMs = DEFAULT_PAYMENT_FETCH_TIMEOUT_MS,
|
|
24
31
|
now = Date.now,
|
|
25
32
|
cache = createCatalogCache(),
|
|
26
33
|
}) {
|
|
@@ -33,7 +40,10 @@ export async function resolveCatalogListings({
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
try {
|
|
36
|
-
const response = await
|
|
43
|
+
const response = await fetchPaymentTarget(config.discoveryUrl, {
|
|
44
|
+
fetchImpl,
|
|
45
|
+
lookup,
|
|
46
|
+
timeoutMs: requestTimeoutMs,
|
|
37
47
|
headers: { accept: 'application/json' },
|
|
38
48
|
});
|
|
39
49
|
const body = await readJson(response);
|
|
@@ -64,7 +74,21 @@ export async function resolveCatalogListings({
|
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
export function createCatalogResolver({
|
|
77
|
+
export function createCatalogResolver({
|
|
78
|
+
config,
|
|
79
|
+
fetchImpl = fetch,
|
|
80
|
+
lookup,
|
|
81
|
+
requestTimeoutMs = DEFAULT_PAYMENT_FETCH_TIMEOUT_MS,
|
|
82
|
+
now = Date.now,
|
|
83
|
+
}) {
|
|
68
84
|
const cache = createCatalogCache();
|
|
69
|
-
return () =>
|
|
85
|
+
return () =>
|
|
86
|
+
resolveCatalogListings({
|
|
87
|
+
config,
|
|
88
|
+
fetchImpl,
|
|
89
|
+
lookup,
|
|
90
|
+
requestTimeoutMs,
|
|
91
|
+
now,
|
|
92
|
+
cache,
|
|
93
|
+
});
|
|
70
94
|
}
|
package/src/client.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
safeErrorMessage,
|
|
8
8
|
} from './guards.mjs';
|
|
9
9
|
import { createSignerFromOptions } from './signer.mjs';
|
|
10
|
+
import { createFileSpendStore } from './spendStore.mjs';
|
|
11
|
+
import { createReceiptSignerResolver } from './receipt.mjs';
|
|
10
12
|
|
|
11
13
|
function isObject(value) {
|
|
12
14
|
return typeof value === 'object' && value !== null;
|
|
@@ -63,14 +65,35 @@ export function createOpenPayClient(options = {}) {
|
|
|
63
65
|
}
|
|
64
66
|
const signer = createSignerFromOptions(options, { fetchImpl });
|
|
65
67
|
const session = createPaymentSession();
|
|
66
|
-
const
|
|
68
|
+
const spendStore =
|
|
69
|
+
config.maxDailyAtomic === null
|
|
70
|
+
? null
|
|
71
|
+
: options.spendStore ?? createFileSpendStore();
|
|
72
|
+
const resolveCatalogListings = createCatalogResolver({
|
|
73
|
+
config,
|
|
74
|
+
fetchImpl,
|
|
75
|
+
lookup: options.lookup,
|
|
76
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
77
|
+
});
|
|
78
|
+
const resolveReceiptSigner = createReceiptSignerResolver({
|
|
79
|
+
discoveryUrl: config.discoveryUrl,
|
|
80
|
+
fetchImpl,
|
|
81
|
+
lookup: options.lookup,
|
|
82
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
83
|
+
});
|
|
67
84
|
const executor = createPaymentExecutor({
|
|
68
85
|
config,
|
|
69
86
|
session,
|
|
70
87
|
signer,
|
|
88
|
+
signerAddress: signer?.address ?? null,
|
|
89
|
+
spendStore,
|
|
71
90
|
fetchImpl,
|
|
91
|
+
lookup: options.lookup,
|
|
92
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
72
93
|
nowSec: options.nowSec,
|
|
94
|
+
now: options.now,
|
|
73
95
|
resolveCatalogListings,
|
|
96
|
+
resolveReceiptSigner,
|
|
74
97
|
});
|
|
75
98
|
|
|
76
99
|
async function withSafeErrors(operation) {
|