uvd-x402-sdk 2.55.0 → 2.56.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/dist/index.d.mts CHANGED
@@ -29,6 +29,8 @@ export { FacilitatorClient, FacilitatorClientOptions, HonoMiddlewareOptions, Pay
29
29
  * Specification: `docs/plans/dx402/02-SPEC-v0.1.md` in x402-rs.
30
30
  */
31
31
  declare const EVIDENCE_HEADER = "X-Durable-Evidence";
32
+ /** Why a party holds a key to this evidence. */
33
+ type RecipientRole = 'payer' | 'seller' | 'auditor';
32
34
  declare class DX402Error extends Error {
33
35
  constructor(message: string);
34
36
  }
@@ -103,14 +105,26 @@ declare function evidenceFromHeaders(headers: Headers | Record<string, string>):
103
105
  * a caller with their own resolver is not blocked by this function.
104
106
  */
105
107
  declare function dereferencePointer(pointer: string): string;
106
- interface SealedEnvelope {
108
+ interface Recipient {
109
+ role: RecipientRole;
107
110
  alg: 'secp256k1' | 'x25519';
108
111
  ephemeral: Uint8Array;
109
112
  cekNonce: Uint8Array;
110
113
  wrappedCek: Uint8Array;
114
+ }
115
+ interface SealedEnvelope {
116
+ recipients: Recipient[];
111
117
  bodyNonce: Uint8Array;
112
118
  ciphertext: Uint8Array;
113
119
  }
120
+ /**
121
+ * Who can open this blob, without decrypting anything.
122
+ *
123
+ * Worth surfacing: a buyer has to be able to see that the seller — or a
124
+ * designated auditor — also holds a key to what they bought. Finding that out
125
+ * afterwards would destroy the privacy property.
126
+ */
127
+ declare function sealedRoles(raw: Uint8Array): RecipientRole[];
114
128
  /**
115
129
  * Parse the sealed-blob layout.
116
130
  *
@@ -121,7 +135,13 @@ interface SealedEnvelope {
121
135
  * rather than an out-of-range surprise later.
122
136
  */
123
137
  declare function parseSealed(raw: Uint8Array): SealedEnvelope;
124
- /** Decrypt a sealed envelope with the payer's private key. */
138
+ /**
139
+ * Decrypt a sealed envelope with whichever recipient slot belongs to `privateKey`.
140
+ *
141
+ * Tries every slot: a holder does not necessarily know which one is theirs, and
142
+ * in a multi-recipient envelope the payer is not always first. A slot that does
143
+ * not open is skipped, not reported — "that one was not for me" is not an error.
144
+ */
125
145
  declare function unseal(sealed: SealedEnvelope, privateKey: Uint8Array, aad: Uint8Array): Uint8Array;
126
146
  /**
127
147
  * Fetch, decrypt and verify the body behind `evidence`.
@@ -821,4 +841,4 @@ interface EscrowPreAuthParams {
821
841
  */
822
842
  declare function buildEscrowPreAuth(wallet: EscrowPreAuthSigner, params: EscrowPreAuthParams): Promise<string>;
823
843
 
824
- export { type AnchoredEvidence, ContentHashMismatch, type CreateSignedFetchConfig, DX402Error, type ERC8128RequestOptions, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, type EscrowNetworkConfig, type EscrowPaymentInfo, type EscrowPreAuthParams, type EscrowPreAuthSigner, type EscrowTierWindows, type EvidenceMode, EvidenceSkipped, FACILITATOR_ADDRESSES, type FacilitatorAddresses, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, type SSEFrame, SSEParser, type SignRequestOptions, type SignRequestWithSignerOptions, type SignatureBaseParams, type SignatureHeaders, type SignatureParamsInput, SigningWalletAdapter, type StreamTrafficEventsOptions, type TrafficEvent, type TrafficEventKind, TrafficStreamError, buildEscrowPreAuth, buildSignatureBase, buildSignatureParams, computeEscrowNonce, contentHash, createSignedFetch, dereferencePointer, paymentId as dx402PaymentId, ed25519ToX25519, evidenceFromHeaders, fetchNonce, getFacilitatorAddress, isEndToEnd, matchesFilters, parseEvidenceHeader, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, recoverEvidence, sealEvidence, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal };
844
+ export { type AnchoredEvidence, ContentHashMismatch, type CreateSignedFetchConfig, DX402Error, type ERC8128RequestOptions, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, type EscrowNetworkConfig, type EscrowPaymentInfo, type EscrowPreAuthParams, type EscrowPreAuthSigner, type EscrowTierWindows, type EvidenceMode, EvidenceSkipped, FACILITATOR_ADDRESSES, type FacilitatorAddresses, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, type RecipientRole, type SSEFrame, SSEParser, type SignRequestOptions, type SignRequestWithSignerOptions, type SignatureBaseParams, type SignatureHeaders, type SignatureParamsInput, SigningWalletAdapter, type StreamTrafficEventsOptions, type TrafficEvent, type TrafficEventKind, TrafficStreamError, buildEscrowPreAuth, buildSignatureBase, buildSignatureParams, computeEscrowNonce, contentHash, createSignedFetch, dereferencePointer, paymentId as dx402PaymentId, ed25519ToX25519, evidenceFromHeaders, fetchNonce, getFacilitatorAddress, isEndToEnd, matchesFilters, parseEvidenceHeader, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, recoverEvidence, sealEvidence, sealedRoles, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal };
package/dist/index.d.ts CHANGED
@@ -29,6 +29,8 @@ export { FacilitatorClient, FacilitatorClientOptions, HonoMiddlewareOptions, Pay
29
29
  * Specification: `docs/plans/dx402/02-SPEC-v0.1.md` in x402-rs.
30
30
  */
31
31
  declare const EVIDENCE_HEADER = "X-Durable-Evidence";
32
+ /** Why a party holds a key to this evidence. */
33
+ type RecipientRole = 'payer' | 'seller' | 'auditor';
32
34
  declare class DX402Error extends Error {
33
35
  constructor(message: string);
34
36
  }
@@ -103,14 +105,26 @@ declare function evidenceFromHeaders(headers: Headers | Record<string, string>):
103
105
  * a caller with their own resolver is not blocked by this function.
104
106
  */
105
107
  declare function dereferencePointer(pointer: string): string;
106
- interface SealedEnvelope {
108
+ interface Recipient {
109
+ role: RecipientRole;
107
110
  alg: 'secp256k1' | 'x25519';
108
111
  ephemeral: Uint8Array;
109
112
  cekNonce: Uint8Array;
110
113
  wrappedCek: Uint8Array;
114
+ }
115
+ interface SealedEnvelope {
116
+ recipients: Recipient[];
111
117
  bodyNonce: Uint8Array;
112
118
  ciphertext: Uint8Array;
113
119
  }
120
+ /**
121
+ * Who can open this blob, without decrypting anything.
122
+ *
123
+ * Worth surfacing: a buyer has to be able to see that the seller — or a
124
+ * designated auditor — also holds a key to what they bought. Finding that out
125
+ * afterwards would destroy the privacy property.
126
+ */
127
+ declare function sealedRoles(raw: Uint8Array): RecipientRole[];
114
128
  /**
115
129
  * Parse the sealed-blob layout.
116
130
  *
@@ -121,7 +135,13 @@ interface SealedEnvelope {
121
135
  * rather than an out-of-range surprise later.
122
136
  */
123
137
  declare function parseSealed(raw: Uint8Array): SealedEnvelope;
124
- /** Decrypt a sealed envelope with the payer's private key. */
138
+ /**
139
+ * Decrypt a sealed envelope with whichever recipient slot belongs to `privateKey`.
140
+ *
141
+ * Tries every slot: a holder does not necessarily know which one is theirs, and
142
+ * in a multi-recipient envelope the payer is not always first. A slot that does
143
+ * not open is skipped, not reported — "that one was not for me" is not an error.
144
+ */
125
145
  declare function unseal(sealed: SealedEnvelope, privateKey: Uint8Array, aad: Uint8Array): Uint8Array;
126
146
  /**
127
147
  * Fetch, decrypt and verify the body behind `evidence`.
@@ -821,4 +841,4 @@ interface EscrowPreAuthParams {
821
841
  */
822
842
  declare function buildEscrowPreAuth(wallet: EscrowPreAuthSigner, params: EscrowPreAuthParams): Promise<string>;
823
843
 
824
- export { type AnchoredEvidence, ContentHashMismatch, type CreateSignedFetchConfig, DX402Error, type ERC8128RequestOptions, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, type EscrowNetworkConfig, type EscrowPaymentInfo, type EscrowPreAuthParams, type EscrowPreAuthSigner, type EscrowTierWindows, type EvidenceMode, EvidenceSkipped, FACILITATOR_ADDRESSES, type FacilitatorAddresses, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, type SSEFrame, SSEParser, type SignRequestOptions, type SignRequestWithSignerOptions, type SignatureBaseParams, type SignatureHeaders, type SignatureParamsInput, SigningWalletAdapter, type StreamTrafficEventsOptions, type TrafficEvent, type TrafficEventKind, TrafficStreamError, buildEscrowPreAuth, buildSignatureBase, buildSignatureParams, computeEscrowNonce, contentHash, createSignedFetch, dereferencePointer, paymentId as dx402PaymentId, ed25519ToX25519, evidenceFromHeaders, fetchNonce, getFacilitatorAddress, isEndToEnd, matchesFilters, parseEvidenceHeader, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, recoverEvidence, sealEvidence, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal };
844
+ export { type AnchoredEvidence, ContentHashMismatch, type CreateSignedFetchConfig, DX402Error, type ERC8128RequestOptions, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, type EscrowNetworkConfig, type EscrowPaymentInfo, type EscrowPreAuthParams, type EscrowPreAuthSigner, type EscrowTierWindows, type EvidenceMode, EvidenceSkipped, FACILITATOR_ADDRESSES, type FacilitatorAddresses, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, type RecipientRole, type SSEFrame, SSEParser, type SignRequestOptions, type SignRequestWithSignerOptions, type SignatureBaseParams, type SignatureHeaders, type SignatureParamsInput, SigningWalletAdapter, type StreamTrafficEventsOptions, type TrafficEvent, type TrafficEventKind, TrafficStreamError, buildEscrowPreAuth, buildSignatureBase, buildSignatureParams, computeEscrowNonce, contentHash, createSignedFetch, dereferencePointer, paymentId as dx402PaymentId, ed25519ToX25519, evidenceFromHeaders, fetchNonce, getFacilitatorAddress, isEndToEnd, matchesFilters, parseEvidenceHeader, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, recoverEvidence, sealEvidence, sealedRoles, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal };
package/dist/index.js CHANGED
@@ -12,7 +12,9 @@ var ethers = require('ethers');
12
12
  // src/dx402.ts
13
13
  var EVIDENCE_HEADER = "X-Durable-Evidence";
14
14
  var MAGIC = new Uint8Array([68, 88, 52, 48, 50]);
15
- var FORMAT_VERSION = 1;
15
+ var FORMAT_V1 = 1;
16
+ var FORMAT_V2 = 2;
17
+ var ROLE_NAMES = ["payer", "seller", "auditor"];
16
18
  var NONCE_LEN = 12;
17
19
  var CEK_LEN = 32;
18
20
  var HKDF_INFO = new TextEncoder().encode("DX402-v1-wrap");
@@ -103,6 +105,9 @@ function dereferencePointer(pointer) {
103
105
  if (pointer.startsWith("ar://")) return `https://arweave.net/${pointer.slice(5)}`;
104
106
  return pointer;
105
107
  }
108
+ function sealedRoles(raw) {
109
+ return parseSealed(raw).recipients.map((r) => r.role);
110
+ }
106
111
  function parseSealed(raw) {
107
112
  let pos = 0;
108
113
  const take = (n, what) => {
@@ -116,29 +121,38 @@ function parseSealed(raw) {
116
121
  if (magic[i] !== MAGIC[i]) throw new DX402Error("not a DX402 sealed blob");
117
122
  }
118
123
  const version = take(1, "version")[0];
119
- if (version !== FORMAT_VERSION) {
120
- throw new DX402Error(`unsupported sealed-blob version ${version}`);
121
- }
122
- const algByte = take(1, "algorithm")[0];
123
- if (algByte !== 1 && algByte !== 2) {
124
- throw new DX402Error(`unknown key algorithm ${algByte}`);
125
- }
126
- const ephLen = take(1, "ephemeral key length")[0];
127
- const ephemeral = take(ephLen, "ephemeral key");
128
- const cekNonce = take(NONCE_LEN, "cek nonce");
129
- const lenBytes = take(2, "wrapped key length");
130
- const wrappedLen = lenBytes[0] << 8 | lenBytes[1];
131
- const wrappedCek = take(wrappedLen, "wrapped cek");
124
+ let count;
125
+ if (version === FORMAT_V1) count = 1;
126
+ else if (version === FORMAT_V2) count = take(1, "recipient count")[0];
127
+ else throw new DX402Error(`unsupported sealed-blob version ${version}`);
128
+ if (count === 0) throw new DX402Error("DX402 sealed blob has no recipients");
129
+ const recipients = [];
130
+ for (let i = 0; i < count; i++) {
131
+ let role = "payer";
132
+ if (version === FORMAT_V2) {
133
+ const b = take(1, "role")[0];
134
+ if (b >= ROLE_NAMES.length) throw new DX402Error(`unknown role ${b}`);
135
+ role = ROLE_NAMES[b];
136
+ }
137
+ const algByte = take(1, "algorithm")[0];
138
+ if (algByte !== 1 && algByte !== 2) {
139
+ throw new DX402Error(`unknown key algorithm ${algByte}`);
140
+ }
141
+ const ephLen = take(1, "ephemeral key length")[0];
142
+ const ephemeral = take(ephLen, "ephemeral key");
143
+ const cekNonce = take(NONCE_LEN, "cek nonce");
144
+ const lenBytes = take(2, "wrapped key length");
145
+ const wrappedCek = take(lenBytes[0] << 8 | lenBytes[1], "wrapped cek");
146
+ recipients.push({
147
+ role,
148
+ alg: algByte === 1 ? "secp256k1" : "x25519",
149
+ ephemeral,
150
+ cekNonce,
151
+ wrappedCek
152
+ });
153
+ }
132
154
  const bodyNonce = take(NONCE_LEN, "body nonce");
133
- const ciphertext = raw.subarray(pos);
134
- return {
135
- alg: algByte === 1 ? "secp256k1" : "x25519",
136
- ephemeral,
137
- cekNonce,
138
- wrappedCek,
139
- bodyNonce,
140
- ciphertext
141
- };
155
+ return { recipients, bodyNonce, ciphertext: raw.subarray(pos) };
142
156
  }
143
157
  function sharedSecret(sealed, privateKey) {
144
158
  if (sealed.alg === "secp256k1") {
@@ -153,13 +167,29 @@ function sharedSecret(sealed, privateKey) {
153
167
  return shared;
154
168
  }
155
169
  function unseal(sealed, privateKey, aad) {
156
- const shared = sharedSecret(sealed, privateKey);
157
- const wrapKey = hkdf.hkdf(sha2.sha256, shared, aad, HKDF_INFO, 32);
158
- const cek = aes.gcm(wrapKey, sealed.cekNonce, aad).decrypt(sealed.wrappedCek);
159
- if (cek.length !== CEK_LEN) {
160
- throw new DX402Error(`unwrapped CEK is ${cek.length} bytes, expected ${CEK_LEN}`);
170
+ for (const recipient of sealed.recipients) {
171
+ let shared;
172
+ try {
173
+ shared = sharedSecret(recipient, privateKey);
174
+ } catch (err) {
175
+ if (err instanceof DX402Error) throw err;
176
+ continue;
177
+ }
178
+ const wrapKey = hkdf.hkdf(sha2.sha256, shared, aad, HKDF_INFO, 32);
179
+ let cek;
180
+ try {
181
+ cek = aes.gcm(wrapKey, recipient.cekNonce, aad).decrypt(recipient.wrappedCek);
182
+ } catch {
183
+ continue;
184
+ }
185
+ if (cek.length !== CEK_LEN) {
186
+ throw new DX402Error(`unwrapped CEK is ${cek.length} bytes, expected ${CEK_LEN}`);
187
+ }
188
+ return aes.gcm(cek, sealed.bodyNonce, aad).decrypt(sealed.ciphertext);
161
189
  }
162
- return aes.gcm(cek, sealed.bodyNonce, aad).decrypt(sealed.ciphertext);
190
+ throw new DX402Error(
191
+ "no recipient slot opened -- wrong key, or the blob belongs to another payment"
192
+ );
163
193
  }
164
194
  async function recoverEvidence(evidence, privateKey, options = {}) {
165
195
  const key = typeof privateKey === "string" ? hexToBytes(privateKey) : privateKey;
@@ -243,7 +273,7 @@ function sealEvidence(body, payerKey, paymentIdValue) {
243
273
  let pos = 0;
244
274
  out.set(MAGIC, pos);
245
275
  pos += MAGIC.length;
246
- out[pos++] = FORMAT_VERSION;
276
+ out[pos++] = FORMAT_V1;
247
277
  out[pos++] = algByte;
248
278
  out[pos++] = ephemeral.length;
249
279
  out.set(ephemeral, pos);
@@ -4132,6 +4162,7 @@ exports.parseTrafficEvent = parseTrafficEvent;
4132
4162
  exports.payerKeyFromEvmSignature = payerKeyFromEvmSignature;
4133
4163
  exports.recoverEvidence = recoverEvidence;
4134
4164
  exports.sealEvidence = sealEvidence;
4165
+ exports.sealedRoles = sealedRoles;
4135
4166
  exports.signRequest = signRequest;
4136
4167
  exports.signRequestWithSigner = signRequestWithSigner;
4137
4168
  exports.signRequestWithWallet = signRequestWithWallet;