openpay-x402-sdk 0.8.0 → 0.9.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-09-21
4
+
5
+ - Add explicit `keystore` signer mode and `wallet_not_initialized` guard reason. `createSigner` requires the caller to supply keystore keys via `createSignerFromOptions`; no env-key fallback.
6
+ - Redact any `0x` + 64-hex value in error text as `[redacted_32byte_hex]` (a key configured through the SDK is still replaced as `[redacted_private_key]`). The generic label is deliberate: the same shape is also a transaction hash or a nonce, and it must not read as "your key leaked". Signature redaction is unchanged. Update public types; existing env-key / Steward behavior is unchanged.
7
+
8
+ ## 0.8.1
9
+
10
+ - Fix `openpay-x402-sdk/delivery` on Cloudflare Workers: the JWKS fetch called a
11
+ detached `fetch`, which Workers reject with "Illegal invocation", so every ticket
12
+ failed with `keys_unavailable`. Verified on a real Worker deployment (2026-09-10).
13
+ - Templates: send `Content-Disposition: attachment; filename="<object key>"` only on
14
+ successful file responses. An error response carrying `attachment` made Chrome
15
+ show ERR_INVALID_RESPONSE instead of the JSON body.
16
+
3
17
  ## 0.8.0
4
18
 
5
19
  - 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
@@ -578,6 +577,20 @@ returned or the facilitator signer could not be resolved. Treat `unverified` and
578
577
 
579
578
  ## Signers
580
579
 
580
+ The environment adapter accepts `SIGNER_MODE=env-key` (default), `steward`, or
581
+ `keystore`. Keystore is an explicit caller-managed mode: `createSigner(env)`
582
+ throws instead of loading a file or falling back to `BUYER_PRIVATE_KEY`. A caller
583
+ such as `openpay-x402-mcp` loads its wallet and uses
584
+ `createSignerFromOptions({ privateKey })`, passing the signer to the executor.
585
+ The keystore guard returns `wallet_not_initialized` when `signerAvailable` is
586
+ false; env-key and Steward retain their existing guard behavior. File storage
587
+ and the default daily limit for keystore are MCP responsibilities.
588
+
589
+ `safeErrorMessage` / `redactSensitiveText` redact 32-byte key hex and 65-byte
590
+ signature hex in error text. Do not apply these functions to payment payloads:
591
+ authorization nonces are also 32-byte hex. OpenPay does not receive, store, or
592
+ recover local wallet keys (OpenPay は鍵を受け取らない・保管しない・復元できない).
593
+
581
594
  Choose exactly one of `privateKey`, `steward`, or `signer`. Supplying more than
582
595
  one is a startup error. A custom signer has an EVM `address` and an async
583
596
  `signTypedData(typedData)` method.
@@ -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/index.d.ts CHANGED
@@ -166,7 +166,7 @@ export type OpenPayClientOptions = ClientCommonOptions &
166
166
  );
167
167
 
168
168
  export interface RuntimeConfig {
169
- signerMode: 'env-key' | 'steward';
169
+ signerMode: 'env-key' | 'steward' | 'keystore';
170
170
  buyerPrivateKey: string | null;
171
171
  stewardApiKey: string | null;
172
172
  stewardSignerSecret: string | null;
@@ -193,6 +193,10 @@ export interface OpenPaySession {
193
193
  }
194
194
 
195
195
  export interface DiscoveryItem {
196
+ /** Short display name (first-party, or seller-provided). */
197
+ title?: string;
198
+ /** One line describing when an agent should buy this resource (optional). */
199
+ trigger?: string;
196
200
  resource: string;
197
201
  description?: string;
198
202
  category?: string;
@@ -698,6 +702,7 @@ export const REASONS: {
698
702
  dailyAuthorizationCrossesUtcDay: 'daily_authorization_crosses_utc_day';
699
703
  buyerPrivateKeyMissing: 'buyer_private_key_missing';
700
704
  stewardSignerUnconfigured: 'steward_signer_unconfigured';
705
+ walletNotInitialized: 'wallet_not_initialized';
701
706
  catalogAcceptMismatch: 'catalog_accept_mismatch';
702
707
  };
703
708
  export const SUPPORTED_JPYC_ASSETS: Readonly<
@@ -822,10 +827,11 @@ export function safeErrorMessage(
822
827
  export const SIGNER_MODES: {
823
828
  envKey: 'env-key';
824
829
  steward: 'steward';
830
+ keystore: 'keystore';
825
831
  };
826
832
  export function readSignerMode(
827
833
  env?: Record<string, string | undefined>,
828
- ): 'env-key' | 'steward';
834
+ ): 'env-key' | 'steward' | 'keystore';
829
835
  export function createSigner(
830
836
  env?: Record<string, string | undefined>,
831
837
  options?: { fetchImpl?: typeof globalThis.fetch },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpay-x402-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
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
  ]);
package/src/guards.mjs CHANGED
@@ -47,6 +47,7 @@ export const REASONS = {
47
47
  dailyAuthorizationCrossesUtcDay: 'daily_authorization_crosses_utc_day',
48
48
  buyerPrivateKeyMissing: 'buyer_private_key_missing',
49
49
  stewardSignerUnconfigured: 'steward_signer_unconfigured',
50
+ walletNotInitialized: 'wallet_not_initialized',
50
51
  // catalog trust 経由 (第三者ドメイン) の URL で、支払い時にライブ fetch した accept が
51
52
  // discovery 掲載 accept (OpenPay サーバー生成の権威値) と食い違う = bait-and-switch。
52
53
  catalogAcceptMismatch: 'catalog_accept_mismatch',
@@ -554,6 +555,8 @@ export function evaluatePaymentGuards({
554
555
  if (requirePrivateKey || requireSigner) {
555
556
  if (config.signerMode === SIGNER_MODES.steward) {
556
557
  if (!signerAvailable) reasons.push(REASONS.stewardSignerUnconfigured);
558
+ } else if (config.signerMode === SIGNER_MODES.keystore) {
559
+ if (!signerAvailable) reasons.push(REASONS.walletNotInitialized);
557
560
  } else if (config.buyerPrivateKey === null) {
558
561
  reasons.push(REASONS.buyerPrivateKeyMissing);
559
562
  }
@@ -574,7 +577,11 @@ export function redactSensitiveText(text, secrets = []) {
574
577
  out = out.split(secret).join('[redacted_private_key]');
575
578
  }
576
579
  }
577
- return out.replace(/\b0x[0-9a-fA-F]{130}\b/g, '[redacted_signature]');
580
+ return out
581
+ .replace(/\b0x[0-9a-fA-F]{130}\b/g, '[redacted_signature]')
582
+ // 32 バイトの hex は秘密鍵と同じ形だが、取引ハッシュや nonce でもあり得る。「鍵が漏れた」と
583
+ // 誤解させないラベルにする (既知の秘密の置換は上の [redacted_private_key] のまま)。
584
+ .replace(/\b0x[0-9a-fA-F]{64}\b/g, '[redacted_32byte_hex]');
578
585
  }
579
586
 
580
587
  export function safeErrorMessage(error, config = {}) {
package/src/signer.mjs CHANGED
@@ -4,6 +4,7 @@ import { privateKeyToAccount } from 'viem/accounts';
4
4
  export const SIGNER_MODES = {
5
5
  envKey: 'env-key',
6
6
  steward: 'steward',
7
+ keystore: 'keystore',
7
8
  };
8
9
 
9
10
  function nonEmpty(raw) {
@@ -12,8 +13,8 @@ function nonEmpty(raw) {
12
13
 
13
14
  export function readSignerMode(env = process.env) {
14
15
  const mode = nonEmpty(env.SIGNER_MODE) ?? SIGNER_MODES.envKey;
15
- if (mode !== SIGNER_MODES.envKey && mode !== SIGNER_MODES.steward) {
16
- throw new Error('SIGNER_MODE must be "env-key" or "steward"');
16
+ if (mode !== SIGNER_MODES.envKey && mode !== SIGNER_MODES.steward && mode !== SIGNER_MODES.keystore) {
17
+ throw new Error('SIGNER_MODE must be "env-key", "steward", or "keystore"');
17
18
  }
18
19
  return mode;
19
20
  }
@@ -182,6 +183,9 @@ function createStewardSigner(env, fetchImpl) {
182
183
 
183
184
  export function createSigner(env = process.env, { fetchImpl = fetch } = {}) {
184
185
  const mode = readSignerMode(env);
186
+ if (mode === SIGNER_MODES.keystore) {
187
+ throw new Error('keystore keys must be supplied by the caller via createSignerFromOptions');
188
+ }
185
189
  if (mode === SIGNER_MODES.envKey) return createEnvKeySigner(env);
186
190
  return createStewardSigner(env, fetchImpl);
187
191
  }