openpay-x402-sdk 0.1.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 +10 -0
- package/README.md +96 -0
- package/index.d.ts +428 -0
- package/package.json +26 -0
- package/src/catalog.mjs +70 -0
- package/src/client.mjs +127 -0
- package/src/executor.mjs +177 -0
- package/src/guards.mjs +443 -0
- package/src/index.mjs +6 -0
- package/src/payment.mjs +232 -0
- package/src/signer.mjs +295 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Add the ESM `createOpenPayClient` API for discovery, free shop lookup, quotes,
|
|
6
|
+
guarded x402 payment, and immutable session snapshots.
|
|
7
|
+
- Add local private-key, seven-field Steward, and custom signer options with an
|
|
8
|
+
exclusive startup contract.
|
|
9
|
+
- Export the payment, guard, signer, catalog, and serialized executor primitives
|
|
10
|
+
with TypeScript declarations.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# openpay-x402-sdk
|
|
2
|
+
|
|
3
|
+
Node.js 20+ SDK for discovering, quoting, and buying OpenPay x402 resources priced
|
|
4
|
+
in JPYC. It ships as plain ESM and has no build step.
|
|
5
|
+
|
|
6
|
+
## Quick start
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install openpay-x402-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { createOpenPayClient } from 'openpay-x402-sdk';
|
|
14
|
+
|
|
15
|
+
const client = createOpenPayClient({
|
|
16
|
+
privateKey: process.env.BUYER_PRIVATE_KEY,
|
|
17
|
+
maxPerCallJpyc: '10',
|
|
18
|
+
maxSessionJpyc: '100',
|
|
19
|
+
allowedHosts: 'open-pay.jp',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const catalog = await client.discover({ query: 'demo' });
|
|
23
|
+
const quote = await client.quote('https://open-pay.jp/api/paid/demo');
|
|
24
|
+
if (quote.ok) {
|
|
25
|
+
const result = await client.pay(quote.url, { maxTotalJpyc: '2' });
|
|
26
|
+
console.log(result.body, client.session);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`discover()` and `findShops({ q?, limit? })` return the server response unchanged
|
|
31
|
+
inside `{ ok, status, body }`. `quote()` fetches and validates a 402 challenge but
|
|
32
|
+
does not need a signer and never pays. `pay()` requires a signer and serializes
|
|
33
|
+
concurrent calls so every call sees the latest session total.
|
|
34
|
+
|
|
35
|
+
## Money guards
|
|
36
|
+
|
|
37
|
+
| Option | Default | Guard |
|
|
38
|
+
|---|---:|---|
|
|
39
|
+
| `maxPerCallJpyc` | `10` | Upper bound for the caller-provided `maxTotalJpyc`. |
|
|
40
|
+
| `maxSessionJpyc` | `100` | Cumulative cap for successful payments made by this client instance. |
|
|
41
|
+
| `allowedHosts` | `open-pay.jp` | Comma-separated bare host allowlist. |
|
|
42
|
+
| `catalogTrust` | `true` | Also allows exact URLs in the discovery catalog after the live challenge matches the catalog challenge. |
|
|
43
|
+
| `discoveryUrl` | `https://open-pay.jp/api/discovery` | Catalog and OpenPay origin used by the client. |
|
|
44
|
+
|
|
45
|
+
`pay(url, { maxTotalJpyc })` always requires `maxTotalJpyc`. It is the maximum
|
|
46
|
+
total—including the resource price and x402 fee—that this individual call is
|
|
47
|
+
authorized to pay. It does not disable or raise `maxPerCallJpyc` or
|
|
48
|
+
`maxSessionJpyc`; all three limits must allow the payment.
|
|
49
|
+
|
|
50
|
+
The client also rejects non-JPYC metadata, unsupported networks or schemes,
|
|
51
|
+
non-OpenPay forwarder splits, amount inconsistencies, resource URL mismatches,
|
|
52
|
+
and catalog bait-and-switches before requesting a signature.
|
|
53
|
+
|
|
54
|
+
## Signers
|
|
55
|
+
|
|
56
|
+
Choose exactly one of `privateKey`, `steward`, or `signer`. Supplying more than
|
|
57
|
+
one is a startup error. A custom signer has an EVM `address` and an async
|
|
58
|
+
`signTypedData(typedData)` method.
|
|
59
|
+
|
|
60
|
+
Steward keeps signing outside the SDK process:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const client = createOpenPayClient({
|
|
64
|
+
steward: {
|
|
65
|
+
url: process.env.STEWARD_URL,
|
|
66
|
+
tenant: process.env.STEWARD_TENANT,
|
|
67
|
+
apiKey: process.env.STEWARD_API_KEY,
|
|
68
|
+
agentId: process.env.STEWARD_AGENT_ID,
|
|
69
|
+
agentAddress: process.env.STEWARD_AGENT_ADDRESS,
|
|
70
|
+
signerId: process.env.STEWARD_SIGNER_ID,
|
|
71
|
+
signerSecret: process.env.STEWARD_SIGNER_SECRET,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The first Steward signature is verified locally against `agentAddress`. Steward
|
|
77
|
+
API keys, signer secrets, and local private keys are redacted from SDK-generated
|
|
78
|
+
errors and are not exposed as client properties.
|
|
79
|
+
|
|
80
|
+
## Security
|
|
81
|
+
|
|
82
|
+
Payments can be irreversible. Use a dedicated low-balance wallet, keep private
|
|
83
|
+
keys and Steward credentials in a secret manager, and never put them in source
|
|
84
|
+
code or logs. Set `maxTotalJpyc` from the amount authorized for the current
|
|
85
|
+
operation, not from the wallet balance. Keep conservative per-call and session
|
|
86
|
+
limits even when Steward applies an additional signing policy.
|
|
87
|
+
|
|
88
|
+
`client.session` returns a new frozen snapshot on every read:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// { spentAtomic: 2000000000000000000n, spentJpyc: '2' }
|
|
92
|
+
console.log(client.session);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Advanced consumers may import the named payment, guard, signer, catalog, and
|
|
96
|
+
executor helpers from the package root.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import type { Address, Hex } from 'viem';
|
|
2
|
+
|
|
3
|
+
export type JpycAmount = string | number;
|
|
4
|
+
|
|
5
|
+
export interface StewardOptions {
|
|
6
|
+
url: string;
|
|
7
|
+
tenant: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
agentId: string;
|
|
10
|
+
agentAddress: string;
|
|
11
|
+
signerId: string;
|
|
12
|
+
signerSecret: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PaymentTypedData {
|
|
16
|
+
domain: {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
chainId: number;
|
|
20
|
+
verifyingContract: Address;
|
|
21
|
+
};
|
|
22
|
+
types: Record<string, readonly { name: string; type: string }[]>;
|
|
23
|
+
primaryType: string;
|
|
24
|
+
message: {
|
|
25
|
+
from: Address;
|
|
26
|
+
to: Address;
|
|
27
|
+
value: bigint;
|
|
28
|
+
validAfter: bigint;
|
|
29
|
+
validBefore: bigint;
|
|
30
|
+
nonce: Hex;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PaymentSigner {
|
|
35
|
+
readonly mode?: string;
|
|
36
|
+
readonly address: Address;
|
|
37
|
+
signTypedData(typedData: PaymentTypedData): Hex | Promise<Hex>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ClientCommonOptions {
|
|
41
|
+
maxPerCallJpyc?: JpycAmount;
|
|
42
|
+
maxSessionJpyc?: JpycAmount;
|
|
43
|
+
allowedHosts?: string;
|
|
44
|
+
catalogTrust?: boolean;
|
|
45
|
+
discoveryUrl?: string;
|
|
46
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type NoSignerOptions = {
|
|
50
|
+
privateKey?: undefined;
|
|
51
|
+
steward?: undefined;
|
|
52
|
+
signer?: undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type PrivateKeyOptions = {
|
|
56
|
+
privateKey: string;
|
|
57
|
+
steward?: never;
|
|
58
|
+
signer?: never;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type StewardSignerOptions = {
|
|
62
|
+
privateKey?: never;
|
|
63
|
+
steward: StewardOptions;
|
|
64
|
+
signer?: never;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type CustomSignerOptions = {
|
|
68
|
+
privateKey?: never;
|
|
69
|
+
steward?: never;
|
|
70
|
+
signer: PaymentSigner;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type OpenPayClientOptions = ClientCommonOptions &
|
|
74
|
+
(
|
|
75
|
+
| NoSignerOptions
|
|
76
|
+
| PrivateKeyOptions
|
|
77
|
+
| StewardSignerOptions
|
|
78
|
+
| CustomSignerOptions
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export interface RuntimeConfig {
|
|
82
|
+
signerMode: 'env-key' | 'steward';
|
|
83
|
+
buyerPrivateKey: string | null;
|
|
84
|
+
stewardApiKey: string | null;
|
|
85
|
+
stewardSignerSecret: string | null;
|
|
86
|
+
maxPerCallAtomic: bigint;
|
|
87
|
+
maxSessionAtomic: bigint;
|
|
88
|
+
allowedHosts: string[];
|
|
89
|
+
catalogTrust: boolean;
|
|
90
|
+
discoveryUrl: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface PaymentSession {
|
|
94
|
+
spentAtomic: bigint;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface OpenPaySession {
|
|
98
|
+
readonly spentAtomic: bigint;
|
|
99
|
+
readonly spentJpyc: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface DiscoveryItem {
|
|
103
|
+
resource: string;
|
|
104
|
+
description?: string;
|
|
105
|
+
category?: string;
|
|
106
|
+
priceJpyc?: string;
|
|
107
|
+
docsUrl?: string;
|
|
108
|
+
license?: string;
|
|
109
|
+
updatedAt?: string;
|
|
110
|
+
network?: string;
|
|
111
|
+
accepts: unknown[];
|
|
112
|
+
verifiedAt?: number | null;
|
|
113
|
+
[key: string]: unknown;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface DiscoveryEnvelope {
|
|
117
|
+
x402Version: number;
|
|
118
|
+
items: DiscoveryItem[];
|
|
119
|
+
[key: string]: unknown;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ShopFindItem {
|
|
123
|
+
handle: string;
|
|
124
|
+
name: string;
|
|
125
|
+
mode: 'storefront' | 'preorder';
|
|
126
|
+
acceptingNow: boolean | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ShopFindEnvelope {
|
|
130
|
+
schemaVersion: '1.0';
|
|
131
|
+
query: { q?: string; limit: number };
|
|
132
|
+
items: ShopFindItem[];
|
|
133
|
+
total: number;
|
|
134
|
+
generatedAt: string;
|
|
135
|
+
dataFreshness: {
|
|
136
|
+
oldestUpdatedAt: string | null;
|
|
137
|
+
newestUpdatedAt: string | null;
|
|
138
|
+
};
|
|
139
|
+
licenseNotice: string;
|
|
140
|
+
attribution: string[];
|
|
141
|
+
[key: string]: unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type FreeApiResult<T> =
|
|
145
|
+
| { ok: true; status: number; body: T }
|
|
146
|
+
| { ok: false; status: number; error: string; body: unknown };
|
|
147
|
+
|
|
148
|
+
export interface GuardedQuote {
|
|
149
|
+
url: string;
|
|
150
|
+
status: number;
|
|
151
|
+
ok: boolean;
|
|
152
|
+
reasons: string[];
|
|
153
|
+
priceJpyc: string | null;
|
|
154
|
+
feeJpyc: string | null;
|
|
155
|
+
totalJpyc: string | null;
|
|
156
|
+
network: string | null;
|
|
157
|
+
asset: Address | null;
|
|
158
|
+
description?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface InvalidChallengeQuote {
|
|
162
|
+
url: string;
|
|
163
|
+
status: number;
|
|
164
|
+
ok: false;
|
|
165
|
+
reasons: ['expected_402_with_accepts'];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type QuoteResult = GuardedQuote | InvalidChallengeQuote;
|
|
169
|
+
|
|
170
|
+
export interface PaymentResult {
|
|
171
|
+
status: number;
|
|
172
|
+
body: unknown;
|
|
173
|
+
receipt: unknown;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface OpenPayClient {
|
|
177
|
+
discover(options?: {
|
|
178
|
+
query?: string;
|
|
179
|
+
category?: string;
|
|
180
|
+
}): Promise<FreeApiResult<DiscoveryEnvelope>>;
|
|
181
|
+
findShops(options?: {
|
|
182
|
+
q?: string;
|
|
183
|
+
limit?: number;
|
|
184
|
+
}): Promise<FreeApiResult<ShopFindEnvelope>>;
|
|
185
|
+
quote(url: string): Promise<QuoteResult>;
|
|
186
|
+
pay(
|
|
187
|
+
url: string,
|
|
188
|
+
options: { maxTotalJpyc: JpycAmount },
|
|
189
|
+
): Promise<QuoteResult | PaymentResult>;
|
|
190
|
+
readonly session: OpenPaySession;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createOpenPayClient(
|
|
194
|
+
options?: OpenPayClientOptions,
|
|
195
|
+
): OpenPayClient;
|
|
196
|
+
|
|
197
|
+
export const RECEIVE_WITH_AUTHORIZATION_TYPES: {
|
|
198
|
+
ReceiveWithAuthorization: Array<{ name: string; type: string }>;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export interface NormalizedPaymentRequirements {
|
|
202
|
+
scheme: 'exact';
|
|
203
|
+
network: string;
|
|
204
|
+
chainId: number;
|
|
205
|
+
asset: Address;
|
|
206
|
+
maxTimeoutSeconds: number;
|
|
207
|
+
extra: {
|
|
208
|
+
name: string;
|
|
209
|
+
version: string;
|
|
210
|
+
openpay: {
|
|
211
|
+
forwarder: Address;
|
|
212
|
+
merchant: Address;
|
|
213
|
+
merchantValue: bigint;
|
|
214
|
+
feeReceiver: Address;
|
|
215
|
+
feeValue: bigint;
|
|
216
|
+
commitVersion: Hex;
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface ForwarderSettleParams {
|
|
222
|
+
from: Address;
|
|
223
|
+
merchant: Address;
|
|
224
|
+
merchantValue: bigint;
|
|
225
|
+
feeReceiver: Address;
|
|
226
|
+
feeValue: bigint;
|
|
227
|
+
validAfter: bigint;
|
|
228
|
+
validBefore: bigint;
|
|
229
|
+
intentSalt: Hex;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface PaymentAuthorization {
|
|
233
|
+
from: Address;
|
|
234
|
+
validAfter: string;
|
|
235
|
+
validBefore: string;
|
|
236
|
+
intentSalt: Hex;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface PaymentPayload {
|
|
240
|
+
x402Version: 1;
|
|
241
|
+
scheme: string;
|
|
242
|
+
network: string;
|
|
243
|
+
payload: {
|
|
244
|
+
signature: Hex;
|
|
245
|
+
authorization: PaymentAuthorization;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function chainIdFromNetwork(network: unknown): number;
|
|
250
|
+
export function normalizePaymentRequirements(
|
|
251
|
+
raw: unknown,
|
|
252
|
+
): NormalizedPaymentRequirements;
|
|
253
|
+
export function buildForwarderNonce(
|
|
254
|
+
params: ForwarderSettleParams,
|
|
255
|
+
chainId: number,
|
|
256
|
+
forwarder: Address,
|
|
257
|
+
commitVersion: Hex,
|
|
258
|
+
): Hex;
|
|
259
|
+
export function buildTypedDataFromPaymentRequirements(
|
|
260
|
+
rawAccept: unknown,
|
|
261
|
+
authorization: PaymentAuthorization,
|
|
262
|
+
): {
|
|
263
|
+
accept: NormalizedPaymentRequirements;
|
|
264
|
+
params: ForwarderSettleParams;
|
|
265
|
+
typedData: PaymentTypedData;
|
|
266
|
+
};
|
|
267
|
+
export function createAuthorization(
|
|
268
|
+
from: Address,
|
|
269
|
+
maxTimeoutSeconds: number,
|
|
270
|
+
nowSec?: number,
|
|
271
|
+
): PaymentAuthorization;
|
|
272
|
+
export function encodePaymentPayload(payload: PaymentPayload): string;
|
|
273
|
+
export function decodePaymentResponse(raw: string | null): unknown;
|
|
274
|
+
export function paymentPayloadFor(
|
|
275
|
+
accept: NormalizedPaymentRequirements,
|
|
276
|
+
authorization: PaymentAuthorization,
|
|
277
|
+
signature: Hex,
|
|
278
|
+
): PaymentPayload;
|
|
279
|
+
|
|
280
|
+
export const JPYC_DECIMALS: 18;
|
|
281
|
+
export const DEFAULT_MAX_PER_CALL_JPYC: '10';
|
|
282
|
+
export const DEFAULT_MAX_SESSION_JPYC: '100';
|
|
283
|
+
export const DEFAULT_ALLOWED_HOSTS: 'open-pay.jp';
|
|
284
|
+
export const DEFAULT_CATALOG_TRUST: true;
|
|
285
|
+
export const DEFAULT_DISCOVERY_URL: 'https://open-pay.jp/api/discovery';
|
|
286
|
+
export const REASONS: {
|
|
287
|
+
invalidUrl: 'invalid_url';
|
|
288
|
+
hostNotAllowed: 'host_not_allowed';
|
|
289
|
+
unsupportedScheme: 'unsupported_scheme';
|
|
290
|
+
unsupportedNetwork: 'unsupported_network';
|
|
291
|
+
invalidOpenpayMode: 'invalid_openpay_mode';
|
|
292
|
+
amountMismatch: 'amount_mismatch';
|
|
293
|
+
invalidJpycAsset: 'invalid_jpyc_asset';
|
|
294
|
+
resourceMismatch: 'resource_mismatch';
|
|
295
|
+
invalidAccept: 'invalid_accept';
|
|
296
|
+
maxTotalRequired: 'max_total_required';
|
|
297
|
+
maxTotalInvalid: 'max_total_invalid';
|
|
298
|
+
totalExceedsMaxTotal: 'total_exceeds_max_total';
|
|
299
|
+
maxTotalAbovePerCallLimit: 'max_total_above_per_call_limit';
|
|
300
|
+
perCallLimitExceeded: 'per_call_limit_exceeded';
|
|
301
|
+
sessionLimitExceeded: 'session_limit_exceeded';
|
|
302
|
+
buyerPrivateKeyMissing: 'buyer_private_key_missing';
|
|
303
|
+
stewardSignerUnconfigured: 'steward_signer_unconfigured';
|
|
304
|
+
catalogAcceptMismatch: 'catalog_accept_mismatch';
|
|
305
|
+
};
|
|
306
|
+
export const SUPPORTED_NETWORKS: Set<string>;
|
|
307
|
+
|
|
308
|
+
export interface AcceptSummary {
|
|
309
|
+
priceAtomic: bigint;
|
|
310
|
+
feeAtomic: bigint;
|
|
311
|
+
totalAtomic: bigint;
|
|
312
|
+
priceJpyc: string;
|
|
313
|
+
feeJpyc: string;
|
|
314
|
+
totalJpyc: string;
|
|
315
|
+
network: string;
|
|
316
|
+
asset: Address;
|
|
317
|
+
description?: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface GuardResult {
|
|
321
|
+
ok: boolean;
|
|
322
|
+
reasons: string[];
|
|
323
|
+
accept: NormalizedPaymentRequirements | null;
|
|
324
|
+
summary: AcceptSummary | null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function parseJpycToAtomic(value: JpycAmount, label: string): bigint;
|
|
328
|
+
export function formatAtomicJpyc(value: bigint): string;
|
|
329
|
+
export function readMoneyConfig(
|
|
330
|
+
env?: Record<string, string | undefined>,
|
|
331
|
+
): Omit<RuntimeConfig, 'discoveryUrl'>;
|
|
332
|
+
export function readRuntimeConfig(
|
|
333
|
+
env?: Record<string, string | undefined>,
|
|
334
|
+
): RuntimeConfig;
|
|
335
|
+
export function parseClientOptions(
|
|
336
|
+
options?: OpenPayClientOptions,
|
|
337
|
+
): RuntimeConfig;
|
|
338
|
+
export function createPaymentSession(initialSpentAtomic?: bigint): PaymentSession;
|
|
339
|
+
export function recordSuccessfulPayment(
|
|
340
|
+
session: PaymentSession,
|
|
341
|
+
amountAtomic: bigint,
|
|
342
|
+
): bigint;
|
|
343
|
+
export function isHostAllowed(url: string, allowedHosts: string[]): boolean;
|
|
344
|
+
export function summarizeAccept(
|
|
345
|
+
accept: NormalizedPaymentRequirements,
|
|
346
|
+
): AcceptSummary;
|
|
347
|
+
export function validateAcceptForPayment(
|
|
348
|
+
rawAccept: unknown,
|
|
349
|
+
requestUrl: string,
|
|
350
|
+
): GuardResult;
|
|
351
|
+
export function evaluatePaymentGuards(options: {
|
|
352
|
+
url: string;
|
|
353
|
+
accept: unknown;
|
|
354
|
+
config: Omit<RuntimeConfig, 'discoveryUrl'> | RuntimeConfig;
|
|
355
|
+
sessionSpentAtomic?: bigint;
|
|
356
|
+
maxTotalJpyc?: JpycAmount;
|
|
357
|
+
requireMaxTotal?: boolean;
|
|
358
|
+
requirePrivateKey?: boolean;
|
|
359
|
+
requireSigner?: boolean;
|
|
360
|
+
signerAvailable?: boolean;
|
|
361
|
+
catalogListings?: Map<string, unknown> | null;
|
|
362
|
+
}): GuardResult;
|
|
363
|
+
export function redactSensitiveText(
|
|
364
|
+
text: unknown,
|
|
365
|
+
secrets?: Array<string | null | undefined>,
|
|
366
|
+
): string;
|
|
367
|
+
export function safeErrorMessage(
|
|
368
|
+
error: unknown,
|
|
369
|
+
config?: Partial<RuntimeConfig>,
|
|
370
|
+
): string;
|
|
371
|
+
|
|
372
|
+
export const SIGNER_MODES: {
|
|
373
|
+
envKey: 'env-key';
|
|
374
|
+
steward: 'steward';
|
|
375
|
+
};
|
|
376
|
+
export function readSignerMode(
|
|
377
|
+
env?: Record<string, string | undefined>,
|
|
378
|
+
): 'env-key' | 'steward';
|
|
379
|
+
export function createSigner(
|
|
380
|
+
env?: Record<string, string | undefined>,
|
|
381
|
+
options?: { fetchImpl?: typeof globalThis.fetch },
|
|
382
|
+
): PaymentSigner;
|
|
383
|
+
export type ParsedSignerOptions =
|
|
384
|
+
| { kind: 'none' }
|
|
385
|
+
| { kind: 'private-key'; privateKey: string }
|
|
386
|
+
| { kind: 'steward'; config: StewardOptions & { agentAddress: Address } }
|
|
387
|
+
| { kind: 'custom'; signer: PaymentSigner; address: Address };
|
|
388
|
+
export function parseSignerOptions(
|
|
389
|
+
options?: OpenPayClientOptions,
|
|
390
|
+
): ParsedSignerOptions;
|
|
391
|
+
export function createSignerFromOptions(
|
|
392
|
+
options?: OpenPayClientOptions,
|
|
393
|
+
runtime?: { fetchImpl?: typeof globalThis.fetch },
|
|
394
|
+
): PaymentSigner | null;
|
|
395
|
+
|
|
396
|
+
export const CATALOG_CACHE_MS: number;
|
|
397
|
+
export interface CatalogCache {
|
|
398
|
+
listings: Map<string, unknown> | null;
|
|
399
|
+
cachedAt: number;
|
|
400
|
+
}
|
|
401
|
+
export function createCatalogCache(): CatalogCache;
|
|
402
|
+
export function resolveCatalogListings(options: {
|
|
403
|
+
config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
|
|
404
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
405
|
+
now?: () => number;
|
|
406
|
+
cache?: CatalogCache;
|
|
407
|
+
}): Promise<Map<string, unknown> | null>;
|
|
408
|
+
export function createCatalogResolver(options: {
|
|
409
|
+
config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
|
|
410
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
411
|
+
now?: () => number;
|
|
412
|
+
}): () => Promise<Map<string, unknown> | null>;
|
|
413
|
+
|
|
414
|
+
export interface PaymentExecutor {
|
|
415
|
+
quote(url: string): Promise<QuoteResult>;
|
|
416
|
+
pay(
|
|
417
|
+
url: string,
|
|
418
|
+
options: { maxTotalJpyc: JpycAmount },
|
|
419
|
+
): Promise<QuoteResult | PaymentResult>;
|
|
420
|
+
}
|
|
421
|
+
export function createPaymentExecutor(options: {
|
|
422
|
+
config: RuntimeConfig;
|
|
423
|
+
session: PaymentSession;
|
|
424
|
+
signer?: PaymentSigner | null;
|
|
425
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
426
|
+
nowSec?: () => number;
|
|
427
|
+
resolveCatalogListings?: () => Promise<Map<string, unknown> | null>;
|
|
428
|
+
}): PaymentExecutor;
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openpay-x402-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Guarded Node.js buyer SDK for OpenPay x402 JPYC resources",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./src/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"viem": "^2.45.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/catalog.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const CATALOG_CACHE_MS = 5 * 60_000;
|
|
2
|
+
|
|
3
|
+
function isObject(value) {
|
|
4
|
+
return typeof value === 'object' && value !== null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function readJson(response) {
|
|
8
|
+
const text = await response.text();
|
|
9
|
+
if (text.length === 0) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(text);
|
|
12
|
+
} catch {
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createCatalogCache() {
|
|
18
|
+
return { listings: null, cachedAt: 0 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function resolveCatalogListings({
|
|
22
|
+
config,
|
|
23
|
+
fetchImpl = fetch,
|
|
24
|
+
now = Date.now,
|
|
25
|
+
cache = createCatalogCache(),
|
|
26
|
+
}) {
|
|
27
|
+
if (!config.catalogTrust) return null;
|
|
28
|
+
if (
|
|
29
|
+
cache.listings !== null &&
|
|
30
|
+
now() - cache.cachedAt < CATALOG_CACHE_MS
|
|
31
|
+
) {
|
|
32
|
+
return cache.listings;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetchImpl(config.discoveryUrl, {
|
|
37
|
+
headers: { accept: 'application/json' },
|
|
38
|
+
});
|
|
39
|
+
const body = await readJson(response);
|
|
40
|
+
if (!response.ok || !isObject(body) || !Array.isArray(body.items)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const listings = new Map();
|
|
44
|
+
for (const item of body.items) {
|
|
45
|
+
if (
|
|
46
|
+
isObject(item) &&
|
|
47
|
+
typeof item.resource === 'string' &&
|
|
48
|
+
Array.isArray(item.accepts) &&
|
|
49
|
+
item.accepts.length > 0
|
|
50
|
+
) {
|
|
51
|
+
try {
|
|
52
|
+
listings.set(new URL(item.resource).toString(), item.accepts[0]);
|
|
53
|
+
} catch {
|
|
54
|
+
// One malformed catalog entry must not hide otherwise valid listings.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
cache.listings = listings;
|
|
59
|
+
cache.cachedAt = now();
|
|
60
|
+
return listings;
|
|
61
|
+
} catch {
|
|
62
|
+
// A discovery outage must not expand trust beyond the explicit host allowlist.
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createCatalogResolver({ config, fetchImpl = fetch, now = Date.now }) {
|
|
68
|
+
const cache = createCatalogCache();
|
|
69
|
+
return () => resolveCatalogListings({ config, fetchImpl, now, cache });
|
|
70
|
+
}
|