openpay-x402-sdk 0.1.0 → 0.2.1

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - Compare `accept.resource` against the requested URL using decoded query
6
+ canonicalization (ordered `URLSearchParams` pairs) instead of byte equality.
7
+ Hosts such as Vercel/Next.js normalize `%20` to `+` before the app sees the
8
+ request, which made honest sellers fail `resource_mismatch`. Distinct decoded
9
+ values (`%2B`, double encoding, reordered or extra params) still mismatch.
10
+
11
+ ## 0.2.0
12
+
13
+ - Trust query-string variants of a query-free catalog URL after the live
14
+ challenge passes the same catalog money-field verification.
15
+ - Keep exact catalog URL matches, explicit host allowlisting, resource matching,
16
+ and public API declarations unchanged.
17
+
3
18
  ## 0.1.0
4
19
 
5
20
  - Add the ESM `createOpenPayClient` API for discovery, free shop lookup, quotes,
package/README.md CHANGED
@@ -39,9 +39,12 @@ concurrent calls so every call sees the latest session total.
39
39
  | `maxPerCallJpyc` | `10` | Upper bound for the caller-provided `maxTotalJpyc`. |
40
40
  | `maxSessionJpyc` | `100` | Cumulative cap for successful payments made by this client instance. |
41
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. |
42
+ | `catalogTrust` | `true` | Also allows catalog URLs after the live challenge matches the catalog challenge. |
43
43
  | `discoveryUrl` | `https://open-pay.jp/api/discovery` | Catalog and OpenPay origin used by the client. |
44
44
 
45
+ Query string variants of a query-free listed URL are trusted after the same
46
+ money-field verification. Exact query-bearing catalog entries remain exact-only.
47
+
45
48
  `pay(url, { maxTotalJpyc })` always requires `maxTotalJpyc`. It is the maximum
46
49
  total—including the resource price and x402 fee—that this individual call is
47
50
  authorized to pay. It does not disable or raise `maxPerCallJpyc` or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpay-x402-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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/guards.mjs CHANGED
@@ -251,6 +251,19 @@ function addAssetReasons(reasons, rawAccept) {
251
251
  }
252
252
  }
253
253
 
254
+ // クエリの正準比較: URLSearchParams でデコードした (key, value) 列の順序付き一致。
255
+ // Vercel/Next 系ホストは request.url の時点でスペースを `+` に正規化するため (`%20` の原文は
256
+ // サーバー側で復元不可能)、accept.resource と要求 URL のバイト一致要求は正当な売り手を
257
+ // 恒常的に落とす (gateway.open-pay.jp で実害)。`%20` と `+` は form-urlencoding で同一の
258
+ // スペースにデコードされる一方、`%2B` (リテラル +) や二重エンコードは異なる値にデコード
259
+ // されるので、この比較は同義エンコーディングだけを同一視し resource 束縛は緩めない。
260
+ function sameQuery(a, b) {
261
+ const ap = [...a.searchParams];
262
+ const bp = [...b.searchParams];
263
+ if (ap.length !== bp.length) return false;
264
+ return ap.every(([k, v], i) => bp[i][0] === k && bp[i][1] === v);
265
+ }
266
+
254
267
  function addResourceReason(reasons, rawAccept, requestUrl) {
255
268
  const requested = parseHttpUrl(requestUrl, 'url');
256
269
  const resource =
@@ -261,7 +274,11 @@ function addResourceReason(reasons, rawAccept, requestUrl) {
261
274
  requested === null ||
262
275
  resource === null ||
263
276
  resource.hostname.toLowerCase() !== requested.hostname.toLowerCase() ||
264
- resource.toString() !== requested.toString()
277
+ resource.origin !== requested.origin ||
278
+ resource.pathname !== requested.pathname ||
279
+ resource.username !== requested.username ||
280
+ resource.password !== requested.password ||
281
+ !sameQuery(resource, requested)
265
282
  ) {
266
283
  reasons.push(REASONS.resourceMismatch);
267
284
  }
@@ -366,11 +383,19 @@ export function evaluatePaymentGuards({
366
383
  reasons.push(REASONS.invalidUrl);
367
384
  } else {
368
385
  const hostAllowed = config.allowedHosts.includes(parsedUrl.hostname.toLowerCase());
369
- // カタログ信頼はホストでなく **URL 完全一致** allowlist より狭い単位で許可する。
370
- const listedAccept =
371
- config.catalogTrust && catalogListings instanceof Map
372
- ? catalogListings.get(parsedUrl.toString())
373
- : undefined;
386
+ // 完全一致を先に維持し、miss 時だけ query/hash を除いた同一 origin+pathname を引く。
387
+ // userinfo は origin に含まれないため、query variant ではない credential 付き URL へは緩和しない。
388
+ let listedAccept;
389
+ if (config.catalogTrust && catalogListings instanceof Map) {
390
+ listedAccept = catalogListings.get(parsedUrl.toString());
391
+ if (
392
+ listedAccept === undefined &&
393
+ parsedUrl.username === '' &&
394
+ parsedUrl.password === ''
395
+ ) {
396
+ listedAccept = catalogListings.get(`${parsedUrl.origin}${parsedUrl.pathname}`);
397
+ }
398
+ }
374
399
  const catalogListed = listedAccept !== undefined;
375
400
  if (!hostAllowed && !catalogListed) {
376
401
  reasons.push(REASONS.hostNotAllowed);