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 +14 -0
- package/README.md +17 -4
- package/examples/cloudflare-r2-delivery-gate/worker.mjs +10 -2
- package/examples/node-delivery-gate.mjs +4 -1
- package/index.d.ts +8 -2
- package/package.json +1 -1
- package/src/delivery.mjs +3 -1
- package/src/guards.mjs +8 -1
- package/src/signer.mjs +6 -2
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
|
|
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
|
|
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
|
|
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',
|
|
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
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
|
-
|
|
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
|
|
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 "
|
|
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
|
}
|