openpay-x402-sdk 0.8.0 → 0.8.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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1
4
+
5
+ - Fix `openpay-x402-sdk/delivery` on Cloudflare Workers: the JWKS fetch called a
6
+ detached `fetch`, which Workers reject with "Illegal invocation", so every ticket
7
+ failed with `keys_unavailable`. Verified on a real Worker deployment (2026-09-10).
8
+ - Templates: send `Content-Disposition: attachment; filename="<object key>"` only on
9
+ successful file responses. An error response carrying `attachment` made Chrome
10
+ show ERR_INVALID_RESPONSE instead of the JSON body.
11
+
3
12
  ## 0.8.0
4
13
 
5
14
  - Add the typed `openpay-x402-sdk/delivery` Web-API-only subpath: strict Ed25519
package/README.md CHANGED
@@ -171,7 +171,7 @@ and is never transmitted.
171
171
 
172
172
  ## 利用ライセンス (License NFT)
173
173
 
174
- SDK 0.7.1 (workspace update; not yet published) resolves the NFT definition from
174
+ SDK 0.7.1 resolves the NFT definition from
175
175
  one product ID. Set only `LICENSE_PRODUCT_ID` and `LICENSE_SESSION_SECRET` on
176
176
  your server. The secret must contain at least 32 random bytes of key material
177
177
  (for example 32 random bytes encoded as hex). Replace the service URLs below
@@ -376,8 +376,7 @@ consumer types.
376
376
 
377
377
  ## 保護配布 (Delivery ticket)
378
378
 
379
- SDK 0.8.0 is an **initial generation** workspace release, not yet published or
380
- production-adopted. OpenPay signs a 60-second bearer ticket after checking
379
+ SDK 0.8.0 adds delivery-ticket verification. OpenPay signs a 60-second bearer ticket after checking
381
380
  entitlement. Sellers verify it with the public JWKS; no secret is shared with
382
381
  OpenPay. The token is signed, not encrypted, and its claims are readable.
383
382
  Possession authorizes admission during its lifetime; it is access control, not
@@ -468,7 +467,7 @@ issuance does not revoke an attacker's signing ability or recall downloaded byte
468
467
  | Node 20.19+ | Global WebCrypto with standard Ed25519; package engine remains Node >=20. |
469
468
  | Node 22.13+ | Same Web API entry point. |
470
469
  | Node 24 | Same Web API entry point. |
471
- | Cloudflare Workers | Standard `Ed25519`, no `nodejs_compat`; run a real deployment smoke on the template's pinned compatibility date. |
470
+ | Cloudflare Workers | Standard `Ed25519`, no `nodejs_compat`. Verified end-to-end on a real Worker + private R2 deployment with SDK 0.8.1 (2026-09-10); re-run the smoke when you change the compatibility date. |
472
471
 
473
472
  `ready()` detects missing Ed25519 support as `unsupported_crypto`; there is no
474
473
  algorithm downgrade. The matrix is a release target, not proof that every runtime
@@ -3,8 +3,16 @@ import { createDeliveryGate } from 'openpay-x402-sdk/delivery';
3
3
  const PRIVATE_HEADERS = {
4
4
  'Cache-Control': 'private, no-store',
5
5
  'Referrer-Policy': 'no-referrer',
6
- 'Content-Disposition': 'attachment',
7
6
  };
7
+ // Only successful file responses are downloads. An error response carrying
8
+ // Content-Disposition: attachment makes Chrome show ERR_INVALID_RESPONSE instead of
9
+ // the JSON body (observed on 2026-09-10). The filename comes from the trusted object
10
+ // key, reduced to a safe ASCII token so no header injection is possible.
11
+ function attachment(key) {
12
+ const base = key.split('/').pop() ?? '';
13
+ const name = base.replace(/[^A-Za-z0-9._-]/g, '_').replace(/^\.+/, '').slice(0, 100) || 'download';
14
+ return `attachment; filename="${name}"`;
15
+ }
8
16
  const instances = new WeakMap();
9
17
 
10
18
  async function startup(env) {
@@ -56,7 +64,7 @@ const worker = {
56
64
  // This small template ignores Range/conditional headers and serves a full
57
65
  // 200 (HEAD returns metadata). Every such request still requires a ticket.
58
66
  return new Response(request.method === 'HEAD' ? null : file.body, {
59
- headers: { ...PRIVATE_HEADERS, 'Content-Type': 'application/octet-stream', 'Content-Length': String(file.size) },
67
+ headers: { ...PRIVATE_HEADERS, 'Content-Disposition': attachment(key), 'Content-Type': 'application/octet-stream', 'Content-Length': String(file.size) },
60
68
  });
61
69
  } catch {
62
70
  // Do not expose request URLs, tickets, R2 keys or upstream exceptions.
@@ -2,8 +2,11 @@ import { createServer } from 'node:http';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { createDeliveryGate } from 'openpay-x402-sdk/delivery';
4
4
 
5
+ // No Content-Disposition here: the redirect target (your presigned URL) decides the
6
+ // download name, and an error response carrying "attachment" makes Chrome show
7
+ // ERR_INVALID_RESPONSE instead of the JSON body.
5
8
  const PRIVATE_HEADERS = {
6
- 'Cache-Control': 'private, no-store', 'Referrer-Policy': 'no-referrer', 'Content-Disposition': 'attachment',
9
+ 'Cache-Control': 'private, no-store', 'Referrer-Policy': 'no-referrer',
7
10
  };
8
11
 
9
12
  async function presignObject({ key, method, expiresAt, expiresInSeconds }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpay-x402-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.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/delivery.mjs CHANGED
@@ -192,7 +192,9 @@ async function refresh(cache, config) {
192
192
  });
193
193
  try {
194
194
  const { keys, age } = await Promise.race([
195
- (async () => readKeysResponse(await config.fetchImpl(`${config.origin}/.well-known/openpay-delivery-keys.json`, {
195
+ // Call with `this` = globalThis: Cloudflare Workers reject a detached `fetch`
196
+ // ("Illegal invocation") — observed on a real Worker on 2026-09-10; Node does not.
197
+ (async () => readKeysResponse(await config.fetchImpl.call(globalThis, `${config.origin}/.well-known/openpay-delivery-keys.json`, {
196
198
  method: 'GET', redirect: 'manual', signal: controller.signal, headers: { accept: 'application/json' },
197
199
  }), config))(), timeout,
198
200
  ]);