@zeroxyz/sdk 0.1.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 +18 -0
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/dist/chunk-7STN754W.cjs +3048 -0
- package/dist/chunk-7STN754W.cjs.map +1 -0
- package/dist/chunk-XQEHJX2X.js +3017 -0
- package/dist/chunk-XQEHJX2X.js.map +1 -0
- package/dist/client-B50UYxPr.d.cts +836 -0
- package/dist/client-B50UYxPr.d.ts +836 -0
- package/dist/index.cjs +150 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.cjs +51 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +14 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +47 -0
- package/dist/testing.js.map +1 -0
- package/package.json +95 -0
- package/testing/package.json +5 -0
|
@@ -0,0 +1,3017 @@
|
|
|
1
|
+
import { serializeTransaction, parseSignature, formatUnits, isAddress, getAddress, parseUnits, createWalletClient, http, createPublicClient, bytesToHex } from 'viem';
|
|
2
|
+
import { toAccount, privateKeyToAccount, mnemonicToAccount } from 'viem/accounts';
|
|
3
|
+
import { getClient, adaptViemWallet, createClient, convertViemChainToRelayChain, MAINNET_RELAY_API } from '@relayprotocol/relay-sdk';
|
|
4
|
+
import { x402Client } from '@x402/core/client';
|
|
5
|
+
import { x402HTTPClient, decodePaymentResponseHeader, encodePaymentRequiredHeader } from '@x402/core/http';
|
|
6
|
+
import { registerExactEvmScheme } from '@x402/evm/exact/client';
|
|
7
|
+
import { createSIWxClientHook } from '@x402/extensions/sign-in-with-x';
|
|
8
|
+
import { wrapFetchWithPayment } from '@x402/fetch';
|
|
9
|
+
import { Receipt, Challenge } from 'mppx';
|
|
10
|
+
import { Mppx, tempo } from 'mppx/client';
|
|
11
|
+
import { base, baseSepolia } from 'viem/chains';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
|
|
15
|
+
// src/auth/managed-account.ts
|
|
16
|
+
var createManagedAccount = (client, address) => toAccount({
|
|
17
|
+
address,
|
|
18
|
+
async signMessage({ message }) {
|
|
19
|
+
return await client.auth.signMessage(
|
|
20
|
+
{ message },
|
|
21
|
+
{ expectedAddress: address }
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
async signTypedData(typedData) {
|
|
25
|
+
const td = typedData;
|
|
26
|
+
return await client.auth.signTypedData(
|
|
27
|
+
{
|
|
28
|
+
domain: td.domain,
|
|
29
|
+
types: td.types,
|
|
30
|
+
primaryType: td.primaryType,
|
|
31
|
+
message: td.message
|
|
32
|
+
},
|
|
33
|
+
{ expectedAddress: address }
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
async signTransaction(transaction, options) {
|
|
37
|
+
const serialize = options?.serializer ?? serializeTransaction;
|
|
38
|
+
const unsigned = await serialize(transaction);
|
|
39
|
+
const signature = await client.auth.signTransaction(
|
|
40
|
+
{ unsignedTransaction: unsigned },
|
|
41
|
+
{ expectedAddress: address }
|
|
42
|
+
);
|
|
43
|
+
return await serialize(transaction, parseSignature(signature));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// src/errors.ts
|
|
48
|
+
var ZeroError = class extends Error {
|
|
49
|
+
code;
|
|
50
|
+
constructor(code, message, options) {
|
|
51
|
+
super(message, options?.cause ? { cause: options.cause } : void 0);
|
|
52
|
+
this.name = "ZeroError";
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var ZeroApiError = class extends ZeroError {
|
|
57
|
+
status;
|
|
58
|
+
requestId;
|
|
59
|
+
body;
|
|
60
|
+
constructor(message, init) {
|
|
61
|
+
super("api_error", message, { cause: init.cause });
|
|
62
|
+
this.name = "ZeroApiError";
|
|
63
|
+
this.status = init.status;
|
|
64
|
+
this.requestId = init.requestId;
|
|
65
|
+
this.body = init.body;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var ZeroAuthError = class extends ZeroError {
|
|
69
|
+
constructor(code, message, options) {
|
|
70
|
+
super(code, message, options);
|
|
71
|
+
this.name = "ZeroAuthError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var ZeroPaymentError = class extends ZeroError {
|
|
75
|
+
constructor(message, options) {
|
|
76
|
+
super("payment_error", message, options);
|
|
77
|
+
this.name = "ZeroPaymentError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var ZeroSessionCloseFailedError = class extends ZeroError {
|
|
81
|
+
session;
|
|
82
|
+
response;
|
|
83
|
+
capturedAmount;
|
|
84
|
+
constructor(message, init) {
|
|
85
|
+
super("payment_session_close_failed", message, { cause: init.cause });
|
|
86
|
+
this.name = "ZeroSessionCloseFailedError";
|
|
87
|
+
this.session = init.session;
|
|
88
|
+
this.response = init.response;
|
|
89
|
+
this.capturedAmount = init.capturedAmount;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var ZeroWalletError = class extends ZeroError {
|
|
93
|
+
constructor(message, options) {
|
|
94
|
+
super("wallet_error", message, options);
|
|
95
|
+
this.name = "ZeroWalletError";
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var ZeroValidationError = class extends ZeroError {
|
|
99
|
+
body;
|
|
100
|
+
constructor(message, options) {
|
|
101
|
+
super("validation_error", message, options);
|
|
102
|
+
this.name = "ZeroValidationError";
|
|
103
|
+
this.body = options?.body;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var ZeroTimeoutError = class extends ZeroError {
|
|
107
|
+
constructor(message, options) {
|
|
108
|
+
super("timeout_error", message, options);
|
|
109
|
+
this.name = "ZeroTimeoutError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var ZeroConfigurationError = class extends ZeroError {
|
|
113
|
+
constructor(message, options) {
|
|
114
|
+
super("configuration_error", message, options);
|
|
115
|
+
this.name = "ZeroConfigurationError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/chains/eip712.ts
|
|
120
|
+
var KNOWN_EIP712_DOMAINS = {
|
|
121
|
+
// Base mainnet USDC. On-chain `name()` is "USD Coin" (NOT "USDC")
|
|
122
|
+
// — verified via the contract's name() / version().
|
|
123
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": {
|
|
124
|
+
name: "USD Coin",
|
|
125
|
+
version: "2"
|
|
126
|
+
},
|
|
127
|
+
// Base Sepolia USDC. Testnet's on-chain `name()` is "USDC",
|
|
128
|
+
// differs from mainnet's "USD Coin".
|
|
129
|
+
"0x036cbd53842c5426634e7929541ec2318f3dcf7e": {
|
|
130
|
+
name: "USDC",
|
|
131
|
+
version: "2"
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var trimmedOrUndefined = (v) => {
|
|
135
|
+
if (typeof v !== "string") return void 0;
|
|
136
|
+
const t = v.trim();
|
|
137
|
+
return t.length > 0 ? t : void 0;
|
|
138
|
+
};
|
|
139
|
+
var trimStringFields = (extra) => {
|
|
140
|
+
if (!extra) return extra;
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
143
|
+
out[k] = typeof v === "string" ? v.trim() : v;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
};
|
|
147
|
+
var withKnownEip712Domain = (asset, extra) => {
|
|
148
|
+
const trimmedExtra = trimStringFields(extra);
|
|
149
|
+
const known = KNOWN_EIP712_DOMAINS[(asset ?? "").toLowerCase()];
|
|
150
|
+
if (!known) return trimmedExtra;
|
|
151
|
+
return {
|
|
152
|
+
...trimmedExtra,
|
|
153
|
+
name: trimmedOrUndefined(trimmedExtra?.name) ?? known.name,
|
|
154
|
+
version: trimmedOrUndefined(trimmedExtra?.version) ?? known.version
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// src/transport/configured-fetch.ts
|
|
159
|
+
var ABORT_SIGNAL_ANY_AVAILABLE = typeof AbortSignal !== "undefined" && typeof AbortSignal.any === "function";
|
|
160
|
+
var headersToRecord = (h) => {
|
|
161
|
+
const result = {};
|
|
162
|
+
h.forEach((value, key) => {
|
|
163
|
+
result[key] = value;
|
|
164
|
+
});
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
var isSameOriginAsBase = (urlString, baseUrl) => {
|
|
168
|
+
try {
|
|
169
|
+
return new URL(urlString).origin === new URL(baseUrl).origin;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var dedupeHeadersInit = (init) => {
|
|
175
|
+
const out = new Headers();
|
|
176
|
+
if (init instanceof Headers) {
|
|
177
|
+
init.forEach((value, key) => {
|
|
178
|
+
out.set(key, value);
|
|
179
|
+
});
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
const entries = Array.isArray(init) ? init : Object.entries(init);
|
|
183
|
+
for (const [key, value] of entries) {
|
|
184
|
+
out.set(key, value);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
};
|
|
188
|
+
var buildConfiguredFetch = (client, overrides) => {
|
|
189
|
+
const timeoutMs = overrides?.timeoutMs ?? client.timeout;
|
|
190
|
+
const impl = async (input, init) => {
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
193
|
+
const isRequestInput = typeof Request !== "undefined" && input instanceof Request;
|
|
194
|
+
const callerSignals = [];
|
|
195
|
+
if (init?.signal) callerSignals.push(init.signal);
|
|
196
|
+
if (isRequestInput && input.signal) callerSignals.push(input.signal);
|
|
197
|
+
const fetchOptionsSignal = client.fetchOptions?.signal;
|
|
198
|
+
if (fetchOptionsSignal) callerSignals.push(fetchOptionsSignal);
|
|
199
|
+
if (callerSignals.length > 0 && !ABORT_SIGNAL_ANY_AVAILABLE) {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
throw new ZeroConfigurationError(
|
|
202
|
+
"AbortSignal.any is required for signal handling and is not available in this runtime. The SDK requires Node 20.3+. Check `process.version` (or your bundler's target)."
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const combinedSignal = callerSignals.length > 0 ? AbortSignal.any([controller.signal, ...callerSignals]) : controller.signal;
|
|
206
|
+
let abortedByCaller = false;
|
|
207
|
+
const onCallerAbort = () => {
|
|
208
|
+
abortedByCaller = true;
|
|
209
|
+
};
|
|
210
|
+
for (const sig of callerSignals) {
|
|
211
|
+
if (sig.aborted) {
|
|
212
|
+
abortedByCaller = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
sig.addEventListener("abort", onCallerAbort, { once: true });
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
let response;
|
|
219
|
+
if (isRequestInput) {
|
|
220
|
+
const injectDefaults = isSameOriginAsBase(input.url, client.baseUrl);
|
|
221
|
+
const merged = new Headers(injectDefaults ? client.defaultHeaders : {});
|
|
222
|
+
if (init?.headers) {
|
|
223
|
+
dedupeHeadersInit(init.headers).forEach((value, key) => {
|
|
224
|
+
merged.set(key, value);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
input.headers.forEach((value, key) => {
|
|
228
|
+
merged.set(key, value);
|
|
229
|
+
});
|
|
230
|
+
const source = input.body !== null ? input.clone() : input;
|
|
231
|
+
const rebuilt = new Request(source, {
|
|
232
|
+
headers: headersToRecord(merged)
|
|
233
|
+
});
|
|
234
|
+
const { headers: _initHeaders, ...initRest } = init ?? {};
|
|
235
|
+
response = await client.fetchImpl(rebuilt, {
|
|
236
|
+
...client.fetchOptions,
|
|
237
|
+
...initRest,
|
|
238
|
+
signal: combinedSignal
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
let mergedHeaders;
|
|
242
|
+
const urlString = typeof input === "string" ? input : String(input);
|
|
243
|
+
const injectDefaults = isSameOriginAsBase(urlString, client.baseUrl);
|
|
244
|
+
const defaults = injectDefaults ? client.defaultHeaders : {};
|
|
245
|
+
const hasDefaults = injectDefaults && Object.keys(defaults).length > 0;
|
|
246
|
+
if (hasDefaults || init?.headers) {
|
|
247
|
+
const merged = new Headers(defaults);
|
|
248
|
+
if (init?.headers) {
|
|
249
|
+
dedupeHeadersInit(init.headers).forEach((value, key) => {
|
|
250
|
+
merged.set(key, value);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
mergedHeaders = headersToRecord(merged);
|
|
254
|
+
}
|
|
255
|
+
response = await client.fetchImpl(input, {
|
|
256
|
+
...client.fetchOptions,
|
|
257
|
+
...init,
|
|
258
|
+
headers: mergedHeaders,
|
|
259
|
+
signal: combinedSignal
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return response;
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
265
|
+
if (abortedByCaller) {
|
|
266
|
+
throw new ZeroApiError("Request aborted by caller", {
|
|
267
|
+
status: 0,
|
|
268
|
+
cause: err
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
throw new ZeroTimeoutError(`Request timed out after ${timeoutMs}ms`, {
|
|
272
|
+
cause: err
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
throw err;
|
|
276
|
+
} finally {
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
for (const sig of callerSignals) {
|
|
279
|
+
sig.removeEventListener("abort", onCallerAbort);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
return impl;
|
|
284
|
+
};
|
|
285
|
+
var looksLikeX402Body = (body) => {
|
|
286
|
+
if (!body || typeof body !== "object") return false;
|
|
287
|
+
const b = body;
|
|
288
|
+
return typeof b.x402Version === "number" && Array.isArray(b.accepts) && b.accepts.length > 0;
|
|
289
|
+
};
|
|
290
|
+
var isV1ShapedAccept = (accept) => {
|
|
291
|
+
if (!accept || typeof accept !== "object") return false;
|
|
292
|
+
const a = accept;
|
|
293
|
+
const network = typeof a.network === "string" ? a.network : "";
|
|
294
|
+
const bareNetwork = network !== "" && !network.includes(":");
|
|
295
|
+
const v1AmountField = a.maxAmountRequired != null && a.amount == null;
|
|
296
|
+
return bareNetwork || v1AmountField;
|
|
297
|
+
};
|
|
298
|
+
var coerceX402BodyVersion = (challenge) => {
|
|
299
|
+
const accepts = Array.isArray(challenge.accepts) ? challenge.accepts : [];
|
|
300
|
+
return { ...challenge, x402Version: accepts.some(isV1ShapedAccept) ? 1 : 2 };
|
|
301
|
+
};
|
|
302
|
+
var withSynthesizedX402Header = (baseFetch) => async (input, init) => {
|
|
303
|
+
const response = await baseFetch(input, init);
|
|
304
|
+
if (response.status !== 402) return response;
|
|
305
|
+
if (response.headers.get("payment-required") ?? response.headers.get("x-payment-required")) {
|
|
306
|
+
return response;
|
|
307
|
+
}
|
|
308
|
+
const text = await response.clone().text();
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
parsed = JSON.parse(text);
|
|
312
|
+
} catch {
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
if (!looksLikeX402Body(parsed) || parsed.x402Version === 1) {
|
|
316
|
+
return response;
|
|
317
|
+
}
|
|
318
|
+
const challenge = coerceX402BodyVersion(parsed);
|
|
319
|
+
const headers = new Headers(response.headers);
|
|
320
|
+
headers.set(
|
|
321
|
+
"PAYMENT-REQUIRED",
|
|
322
|
+
// Validated as an x402 body above; encoder wants the concrete type.
|
|
323
|
+
encodePaymentRequiredHeader(
|
|
324
|
+
challenge
|
|
325
|
+
)
|
|
326
|
+
);
|
|
327
|
+
headers.delete("content-length");
|
|
328
|
+
headers.delete("content-encoding");
|
|
329
|
+
return new Response(text, {
|
|
330
|
+
status: response.status,
|
|
331
|
+
statusText: response.statusText,
|
|
332
|
+
headers
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// src/namespaces/payments.ts
|
|
337
|
+
var NETWORK_TO_CHAIN = {
|
|
338
|
+
base: "base",
|
|
339
|
+
"base-sepolia": "base-sepolia",
|
|
340
|
+
"eip155:8453": "base",
|
|
341
|
+
"eip155:84532": "base-sepolia"
|
|
342
|
+
};
|
|
343
|
+
var networkToChain = (network) => {
|
|
344
|
+
if (!network) return "unknown";
|
|
345
|
+
const normalized = network.toLowerCase();
|
|
346
|
+
return NETWORK_TO_CHAIN[normalized] ?? "unknown";
|
|
347
|
+
};
|
|
348
|
+
var MAX_PAY_RE = /^[0-9]+(\.[0-9]{1,6})?$/;
|
|
349
|
+
var UINT_RE = /^[0-9]+$/;
|
|
350
|
+
var MAX_AMOUNT_DIGITS = 30;
|
|
351
|
+
var USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
352
|
+
var USDC_TEMPO = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
353
|
+
var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
|
|
354
|
+
var BASE_CHAIN_ID = 8453;
|
|
355
|
+
var TEMPO_CHAIN_ID = 4217;
|
|
356
|
+
var TEMPO_TESTNET_CHAIN_ID = 42431;
|
|
357
|
+
var DEFAULT_PREFUND = "1";
|
|
358
|
+
var tempoChain = {
|
|
359
|
+
id: TEMPO_CHAIN_ID,
|
|
360
|
+
name: "Tempo",
|
|
361
|
+
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
362
|
+
rpcUrls: { default: { http: ["https://rpc.tempo.xyz"] } }
|
|
363
|
+
};
|
|
364
|
+
var tempoTestnetChain = {
|
|
365
|
+
id: TEMPO_TESTNET_CHAIN_ID,
|
|
366
|
+
name: "Tempo Testnet",
|
|
367
|
+
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
368
|
+
rpcUrls: { default: { http: ["https://rpc.moderato.tempo.xyz"] } }
|
|
369
|
+
};
|
|
370
|
+
var ERC20_BALANCE_ABI = [
|
|
371
|
+
{
|
|
372
|
+
inputs: [{ name: "account", type: "address" }],
|
|
373
|
+
name: "balanceOf",
|
|
374
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
375
|
+
stateMutability: "view",
|
|
376
|
+
type: "function"
|
|
377
|
+
}
|
|
378
|
+
];
|
|
379
|
+
var calculateBuffer = (baseBalance) => {
|
|
380
|
+
const twentyFivePercent = baseBalance / 4n;
|
|
381
|
+
const twoDollars = 2000000n;
|
|
382
|
+
return twentyFivePercent < twoDollars ? twentyFivePercent : twoDollars;
|
|
383
|
+
};
|
|
384
|
+
var ADAPTIVE_PREFUND_MULTIPLIER = 10;
|
|
385
|
+
var ADAPTIVE_PREFUND_CEILING = "100";
|
|
386
|
+
var resolveDefaultPrefund = (displayCostAmount) => {
|
|
387
|
+
const floor = Number.parseFloat(DEFAULT_PREFUND);
|
|
388
|
+
const ceiling = Number.parseFloat(ADAPTIVE_PREFUND_CEILING);
|
|
389
|
+
if (!displayCostAmount) return DEFAULT_PREFUND;
|
|
390
|
+
const cost = Number.parseFloat(displayCostAmount);
|
|
391
|
+
if (!Number.isFinite(cost) || cost <= 0) return DEFAULT_PREFUND;
|
|
392
|
+
const scaled = cost * ADAPTIVE_PREFUND_MULTIPLIER;
|
|
393
|
+
const bounded = scaled < floor ? floor : scaled > ceiling ? ceiling : scaled;
|
|
394
|
+
return bounded.toFixed(6).replace(/\.?0+$/, "");
|
|
395
|
+
};
|
|
396
|
+
var isUintString = (v) => typeof v === "string" && v.length > 0 && v.length <= MAX_AMOUNT_DIGITS && UINT_RE.test(v);
|
|
397
|
+
var coerceTempoChainId = (raw) => {
|
|
398
|
+
const id = typeof raw === "number" && Number.isFinite(raw) ? raw : typeof raw === "string" && /^-?[0-9]+$/.test(raw) ? Number(raw) : null;
|
|
399
|
+
if (id === TEMPO_CHAIN_ID) return { id, chain: "tempo" };
|
|
400
|
+
if (id === TEMPO_TESTNET_CHAIN_ID) return { id, chain: "tempo-testnet" };
|
|
401
|
+
return null;
|
|
402
|
+
};
|
|
403
|
+
var tempoChainLabelFromId = (chainId) => {
|
|
404
|
+
const ok = coerceTempoChainId(chainId);
|
|
405
|
+
return ok ? ok.chain : "unknown";
|
|
406
|
+
};
|
|
407
|
+
var paymentHasAnchor = (payment) => payment !== null && (payment.txHash !== null || payment.session !== void 0);
|
|
408
|
+
var FETCH_WARNINGS = Object.freeze({
|
|
409
|
+
paidButStill402NotRecorded: "paid_but_still_402_not_recorded",
|
|
410
|
+
paymentSettlementUnconfirmed: "payment_settlement_unconfirmed",
|
|
411
|
+
paymentAmountUnknown: "payment_amount_unknown",
|
|
412
|
+
mppSessionCloseFailed: "mpp_session_close_failed",
|
|
413
|
+
// MPP session-intent challenge but onChannelUpdate never observed opened=true
|
|
414
|
+
// before mppx.fetch resolved — a session call may have been recorded as
|
|
415
|
+
// charge by accident. Surface for investigation.
|
|
416
|
+
sessionOpenUnobserved: "session_open_unobserved",
|
|
417
|
+
// Response body exceeded maxResponseBytes — bodyRaw / body / bodyEncoding
|
|
418
|
+
// reflect the clipped prefix; consumers that wrote the body to disk got
|
|
419
|
+
// a truncated file. Particularly bad for paid binary fetches.
|
|
420
|
+
bodyTruncated: "body_truncated"
|
|
421
|
+
});
|
|
422
|
+
var FETCH_SKIP_REASONS = Object.freeze({
|
|
423
|
+
bodyReadFailed: "body_read_failed",
|
|
424
|
+
mppSessionCloseFailedNotRecorded: "mpp_session_close_failed_not_recorded",
|
|
425
|
+
paidButStill402NotRecorded: FETCH_WARNINGS.paidButStill402NotRecorded,
|
|
426
|
+
paymentSettlementUnconfirmed: FETCH_WARNINGS.paymentSettlementUnconfirmed
|
|
427
|
+
});
|
|
428
|
+
var MAX_RECEIPT_HEADER_BYTES = 8 * 1024;
|
|
429
|
+
var decodeSessionReceiptHeader = (header) => {
|
|
430
|
+
if (!header) return null;
|
|
431
|
+
if (header.length > MAX_RECEIPT_HEADER_BYTES) return null;
|
|
432
|
+
try {
|
|
433
|
+
const padLen = (4 - header.length % 4) % 4;
|
|
434
|
+
const padded = header.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(padLen);
|
|
435
|
+
const json = Buffer.from(padded, "base64").toString("utf8");
|
|
436
|
+
const parsed = JSON.parse(json);
|
|
437
|
+
if (typeof parsed.channelId !== "string" || typeof parsed.challengeId !== "string" || !isUintString(parsed.acceptedCumulative) || !isUintString(parsed.spent)) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const out = {
|
|
441
|
+
channelId: parsed.channelId,
|
|
442
|
+
challengeId: parsed.challengeId,
|
|
443
|
+
acceptedCumulative: parsed.acceptedCumulative,
|
|
444
|
+
spent: parsed.spent
|
|
445
|
+
};
|
|
446
|
+
if (typeof parsed.txHash === "string") out.txHash = parsed.txHash;
|
|
447
|
+
if (typeof parsed.metered === "boolean") out.metered = parsed.metered;
|
|
448
|
+
return out;
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var readSessionReceipt = (response) => decodeSessionReceiptHeader(
|
|
454
|
+
response.headers.get("Payment-Receipt") ?? response.headers.get("payment-receipt")
|
|
455
|
+
);
|
|
456
|
+
var pickSessionCloseAmount = (receipt, openTimeCumulative) => {
|
|
457
|
+
if (!receipt) return openTimeCumulative;
|
|
458
|
+
const accepted = BigInt(receipt.acceptedCumulative);
|
|
459
|
+
const spent = BigInt(receipt.spent);
|
|
460
|
+
const fromReceipt = accepted > spent ? accepted : spent;
|
|
461
|
+
if (receipt.metered === true) return fromReceipt;
|
|
462
|
+
return fromReceipt > openTimeCumulative ? fromReceipt : openTimeCumulative;
|
|
463
|
+
};
|
|
464
|
+
var raceWithSignal = (p, signal, client) => {
|
|
465
|
+
if (!signal) return p;
|
|
466
|
+
if (signal.aborted) {
|
|
467
|
+
p.catch((err) => {
|
|
468
|
+
client.logger?.({
|
|
469
|
+
level: "warn",
|
|
470
|
+
message: "Post-abort rejection from in-flight work (dropped to caller)",
|
|
471
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
475
|
+
}
|
|
476
|
+
return new Promise((resolve, reject) => {
|
|
477
|
+
let settled = false;
|
|
478
|
+
const onAbort = () => {
|
|
479
|
+
settled = true;
|
|
480
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
481
|
+
};
|
|
482
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
483
|
+
p.then(
|
|
484
|
+
(v) => {
|
|
485
|
+
signal.removeEventListener("abort", onAbort);
|
|
486
|
+
if (settled) return;
|
|
487
|
+
settled = true;
|
|
488
|
+
resolve(v);
|
|
489
|
+
},
|
|
490
|
+
(e) => {
|
|
491
|
+
signal.removeEventListener("abort", onAbort);
|
|
492
|
+
if (settled) {
|
|
493
|
+
client.logger?.({
|
|
494
|
+
level: "warn",
|
|
495
|
+
message: "Post-abort rejection from in-flight work (dropped to caller)",
|
|
496
|
+
meta: { error: e instanceof Error ? e.message : String(e) }
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
settled = true;
|
|
501
|
+
reject(e);
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
var cancelBody = (response) => {
|
|
507
|
+
try {
|
|
508
|
+
response.body?.cancel().catch(() => {
|
|
509
|
+
});
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
var resolveAccount = async (client, overrideAccount, signal) => {
|
|
514
|
+
if (overrideAccount) return overrideAccount;
|
|
515
|
+
const credentials = client.getCredentials();
|
|
516
|
+
if (credentials.kind === "account") return credentials.account;
|
|
517
|
+
if (credentials.kind === "session") {
|
|
518
|
+
return await client.resolveManagedAccount(signal);
|
|
519
|
+
}
|
|
520
|
+
throw new ZeroAuthError(
|
|
521
|
+
"auth_no_credentials",
|
|
522
|
+
"payments.pay() requires either a client constructed with `account` or a per-call `account` override."
|
|
523
|
+
);
|
|
524
|
+
};
|
|
525
|
+
var validateMaxPay = (maxPay) => {
|
|
526
|
+
const normalized = maxPay?.trim();
|
|
527
|
+
if (normalized === void 0) return null;
|
|
528
|
+
if (!MAX_PAY_RE.test(normalized)) {
|
|
529
|
+
throw new ZeroPaymentError(
|
|
530
|
+
`Invalid maxPay format: ${JSON.stringify(maxPay)}. Expected a decimal USDC string with up to 6 fractional digits, like "0.10". Scientific notation, currency suffixes, and >6-decimal precision are rejected.`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return parseUnits(normalized, 6);
|
|
534
|
+
};
|
|
535
|
+
var Payments = class {
|
|
536
|
+
constructor(client) {
|
|
537
|
+
this.client = client;
|
|
538
|
+
}
|
|
539
|
+
relayInitialized = false;
|
|
540
|
+
/**
|
|
541
|
+
* Settle an x402 payment for a single request.
|
|
542
|
+
*
|
|
543
|
+
* Routes through `@x402/fetch`, which handles the full 402 → sign →
|
|
544
|
+
* retry round-trip. The settlement transaction hash (when present in
|
|
545
|
+
* the `payment-response` header AND the facilitator reports success)
|
|
546
|
+
* is extracted into the returned `payment` metadata.
|
|
547
|
+
*
|
|
548
|
+
* Aborts before signing if the server's challenge amount exceeds
|
|
549
|
+
* `maxPay`. The cap is denominated in USDC (the asset of every Zero
|
|
550
|
+
* payment today); the API will gain a `maxPayAsset` field if other
|
|
551
|
+
* assets are added.
|
|
552
|
+
*
|
|
553
|
+
* MPP capabilities use `payMpp()` instead — `client.fetch` routes based
|
|
554
|
+
* on the detected 402 protocol.
|
|
555
|
+
*/
|
|
556
|
+
pay = async (input) => {
|
|
557
|
+
const maxPayUnits = validateMaxPay(input.maxPay);
|
|
558
|
+
if (maxPayUnits === 0n) {
|
|
559
|
+
throw new ZeroPaymentError("maxPay='0' disallows any payment.");
|
|
560
|
+
}
|
|
561
|
+
const account = await resolveAccount(
|
|
562
|
+
this.client,
|
|
563
|
+
input.account,
|
|
564
|
+
input.signal
|
|
565
|
+
);
|
|
566
|
+
let capturedAmount = null;
|
|
567
|
+
let capturedNetwork;
|
|
568
|
+
const x402 = registerExactEvmScheme(new x402Client(), { signer: account });
|
|
569
|
+
x402.onBeforePaymentCreation(async (context) => {
|
|
570
|
+
const selected = context.selectedRequirements;
|
|
571
|
+
if (selected) {
|
|
572
|
+
const filled = withKnownEip712Domain(selected.asset, selected.extra);
|
|
573
|
+
if (filled) selected.extra = filled;
|
|
574
|
+
}
|
|
575
|
+
const requirement = selected ?? context.paymentRequired.accepts[0];
|
|
576
|
+
if (!requirement) {
|
|
577
|
+
return {
|
|
578
|
+
abort: true,
|
|
579
|
+
reason: "x402 challenge had no payment requirements"
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
capturedNetwork = requirement.network;
|
|
583
|
+
const rawAmount = requirement.amount ?? requirement.maxAmountRequired;
|
|
584
|
+
if (!rawAmount) {
|
|
585
|
+
return { abort: true, reason: "x402 requirement had no amount field" };
|
|
586
|
+
}
|
|
587
|
+
if (rawAmount.length > MAX_AMOUNT_DIGITS || !UINT_RE.test(rawAmount)) {
|
|
588
|
+
return {
|
|
589
|
+
abort: true,
|
|
590
|
+
reason: `x402 requirement amount is not an unsigned integer string: ${JSON.stringify(rawAmount)}`
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const rawAmountUnits = BigInt(rawAmount);
|
|
594
|
+
capturedAmount = formatUnits(rawAmountUnits, 6);
|
|
595
|
+
if (maxPayUnits !== null && rawAmountUnits > maxPayUnits) {
|
|
596
|
+
return {
|
|
597
|
+
abort: true,
|
|
598
|
+
reason: `Payment of ${capturedAmount} USDC exceeds maxPay ${input.maxPay}`
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
const configuredFetch = buildConfiguredFetch(this.client, {
|
|
603
|
+
timeoutMs: input.timeoutMs
|
|
604
|
+
});
|
|
605
|
+
const httpClient = new x402HTTPClient(x402).onPaymentRequired(
|
|
606
|
+
createSIWxClientHook(account)
|
|
607
|
+
);
|
|
608
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
609
|
+
withSynthesizedX402Header(configuredFetch),
|
|
610
|
+
httpClient
|
|
611
|
+
);
|
|
612
|
+
let response;
|
|
613
|
+
try {
|
|
614
|
+
response = await wrappedFetch(input.url, {
|
|
615
|
+
method: input.method ?? "GET",
|
|
616
|
+
headers: input.headers,
|
|
617
|
+
body: input.body,
|
|
618
|
+
signal: input.signal
|
|
619
|
+
});
|
|
620
|
+
} catch (err) {
|
|
621
|
+
if (err instanceof ZeroError) throw err;
|
|
622
|
+
throw new ZeroPaymentError(
|
|
623
|
+
`x402 payment failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
624
|
+
{ cause: err }
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
let txHash = null;
|
|
628
|
+
const settlementHeader = response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
|
|
629
|
+
if (settlementHeader) {
|
|
630
|
+
try {
|
|
631
|
+
const settlement = decodePaymentResponseHeader(settlementHeader);
|
|
632
|
+
if (settlement.success && typeof settlement.transaction === "string" && settlement.transaction.length > 0) {
|
|
633
|
+
txHash = settlement.transaction;
|
|
634
|
+
}
|
|
635
|
+
if (capturedNetwork === void 0 && typeof settlement.network === "string") {
|
|
636
|
+
capturedNetwork = settlement.network;
|
|
637
|
+
}
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const amount = capturedAmount ?? "unknown";
|
|
642
|
+
return {
|
|
643
|
+
response,
|
|
644
|
+
payment: {
|
|
645
|
+
protocol: "x402",
|
|
646
|
+
chain: networkToChain(capturedNetwork),
|
|
647
|
+
txHash,
|
|
648
|
+
amount,
|
|
649
|
+
asset: "USDC"
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
};
|
|
653
|
+
/**
|
|
654
|
+
* @internal — called by `client.fetch` after MPP protocol detection.
|
|
655
|
+
*
|
|
656
|
+
* Runs a single `mppx.fetch` against the Tempo payment-channel protocol.
|
|
657
|
+
* In-band balance/bridge logic runs from mppx's `onChallenge` callback so
|
|
658
|
+
* the lifecycle is a single HTTP round-trip from the SDK consumer's view
|
|
659
|
+
* (no pre-probe — session facilitators that re-run inference per request
|
|
660
|
+
* can't reconcile the extra state).
|
|
661
|
+
*
|
|
662
|
+
* Session-intent capabilities (long-lived channels) close their voucher
|
|
663
|
+
* after the seller's 200; close failures throw `ZeroSessionCloseFailedError`
|
|
664
|
+
* carrying the orphaned channel for out-of-band reconciliation.
|
|
665
|
+
*/
|
|
666
|
+
payMpp = async (input) => {
|
|
667
|
+
const maxPayUnits = validateMaxPay(input.maxPay);
|
|
668
|
+
if (maxPayUnits === 0n) {
|
|
669
|
+
throw new ZeroPaymentError("maxPay='0' disallows any payment.");
|
|
670
|
+
}
|
|
671
|
+
const account = await resolveAccount(
|
|
672
|
+
this.client,
|
|
673
|
+
input.account,
|
|
674
|
+
input.signal
|
|
675
|
+
);
|
|
676
|
+
const configuredFetch = buildConfiguredFetch(this.client, {
|
|
677
|
+
timeoutMs: input.timeoutMs
|
|
678
|
+
});
|
|
679
|
+
let capturedAmount = "unknown";
|
|
680
|
+
let capturedChallenge;
|
|
681
|
+
let channelEntry;
|
|
682
|
+
let openTimeCumulative;
|
|
683
|
+
const mppx = Mppx.create({
|
|
684
|
+
polyfill: false,
|
|
685
|
+
fetch: configuredFetch,
|
|
686
|
+
methods: [
|
|
687
|
+
tempo({
|
|
688
|
+
account,
|
|
689
|
+
maxDeposit: input.maxPay ?? resolveDefaultPrefund(input.displayCostAmount),
|
|
690
|
+
onChannelUpdate: (entry) => {
|
|
691
|
+
let cumulative;
|
|
692
|
+
if (typeof entry.cumulativeAmount === "bigint") {
|
|
693
|
+
cumulative = entry.cumulativeAmount;
|
|
694
|
+
} else {
|
|
695
|
+
cumulative = 0n;
|
|
696
|
+
this.client.logger?.({
|
|
697
|
+
level: "warn",
|
|
698
|
+
message: "mppx emitted non-bigint cumulativeAmount; coerced to 0n",
|
|
699
|
+
meta: { actualType: typeof entry.cumulativeAmount }
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (openTimeCumulative === void 0 && entry.opened) {
|
|
703
|
+
openTimeCumulative = cumulative;
|
|
704
|
+
}
|
|
705
|
+
const wasOpened = channelEntry?.opened ?? false;
|
|
706
|
+
channelEntry = {
|
|
707
|
+
channelId: entry.channelId,
|
|
708
|
+
escrowContract: entry.escrowContract,
|
|
709
|
+
cumulativeAmount: cumulative,
|
|
710
|
+
opened: wasOpened || entry.opened
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
],
|
|
715
|
+
onChallenge: async (challenge) => {
|
|
716
|
+
capturedChallenge = challenge;
|
|
717
|
+
capturedAmount = await this.prepareTempoFunds(
|
|
718
|
+
account,
|
|
719
|
+
challenge,
|
|
720
|
+
maxPayUnits,
|
|
721
|
+
input.displayCostAmount
|
|
722
|
+
);
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
let response;
|
|
727
|
+
try {
|
|
728
|
+
response = await mppx.fetch(input.url, {
|
|
729
|
+
method: input.method ?? "GET",
|
|
730
|
+
headers: input.headers,
|
|
731
|
+
body: input.body,
|
|
732
|
+
signal: input.signal
|
|
733
|
+
});
|
|
734
|
+
} catch (err) {
|
|
735
|
+
if (channelEntry?.opened) {
|
|
736
|
+
throw this.buildOrphanError(
|
|
737
|
+
channelEntry,
|
|
738
|
+
capturedChallenge,
|
|
739
|
+
capturedAmount,
|
|
740
|
+
new Response(null, { status: 502 }),
|
|
741
|
+
`MPP payment failed after channel open: ${err instanceof Error ? err.message : String(err)}`,
|
|
742
|
+
err
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
if (err instanceof ZeroError) throw err;
|
|
746
|
+
throw new ZeroPaymentError(
|
|
747
|
+
`MPP payment failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
748
|
+
{ cause: err }
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (capturedChallenge?.intent === "session" && channelEntry?.opened) {
|
|
752
|
+
return this.completeMppSession({
|
|
753
|
+
url: input.url,
|
|
754
|
+
method: input.method ?? "GET",
|
|
755
|
+
challenge: capturedChallenge,
|
|
756
|
+
channelEntry,
|
|
757
|
+
openTimeCumulative: openTimeCumulative ?? channelEntry.cumulativeAmount,
|
|
758
|
+
response,
|
|
759
|
+
capturedAmount,
|
|
760
|
+
mppx,
|
|
761
|
+
signal: input.signal,
|
|
762
|
+
configuredFetch
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const sdkWarnings = [];
|
|
766
|
+
if (capturedChallenge?.intent === "session" && !channelEntry?.opened) {
|
|
767
|
+
this.client.logger?.({
|
|
768
|
+
level: "warn",
|
|
769
|
+
message: "MPP session challenge but onChannelUpdate never observed opened=true; recording as charge."
|
|
770
|
+
});
|
|
771
|
+
sdkWarnings.push(FETCH_WARNINGS.sessionOpenUnobserved);
|
|
772
|
+
}
|
|
773
|
+
let txHash = null;
|
|
774
|
+
try {
|
|
775
|
+
const receipt = Receipt.fromResponse(response);
|
|
776
|
+
txHash = receipt.reference ?? null;
|
|
777
|
+
} catch (err) {
|
|
778
|
+
this.client.logger?.({
|
|
779
|
+
level: "warn",
|
|
780
|
+
message: "Failed to parse MPP charge receipt; txHash unknown",
|
|
781
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
response,
|
|
786
|
+
...sdkWarnings.length > 0 && { warnings: sdkWarnings },
|
|
787
|
+
payment: {
|
|
788
|
+
protocol: "mpp",
|
|
789
|
+
chain: this.tempoChainLabel(capturedChallenge),
|
|
790
|
+
txHash,
|
|
791
|
+
amount: capturedAmount,
|
|
792
|
+
asset: "USDC"
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
};
|
|
796
|
+
// Construct a ZeroSessionCloseFailedError carrying orphan metadata derived
|
|
797
|
+
// from the locally-known channelEntry. Used by post-open error paths so the
|
|
798
|
+
// caller can reconcile the funded channel even when the failure happened
|
|
799
|
+
// before completeMppSession had a chance to build its full session record.
|
|
800
|
+
buildOrphanError = (channelEntry, challenge, capturedAmount, response, message, cause) => {
|
|
801
|
+
const req = challenge?.request ?? {};
|
|
802
|
+
const methodDetails = req.methodDetails;
|
|
803
|
+
const rawChainId = req.chainId ?? methodDetails?.chainId;
|
|
804
|
+
const coercedChain = coerceTempoChainId(rawChainId);
|
|
805
|
+
const chainId = coercedChain?.id ?? (typeof rawChainId === "number" ? rawChainId : 0);
|
|
806
|
+
const recipientRaw = req.recipient;
|
|
807
|
+
const escrowRaw = methodDetails?.escrowContract;
|
|
808
|
+
const recipient = typeof recipientRaw === "string" && isAddress(recipientRaw) ? getAddress(recipientRaw) : null;
|
|
809
|
+
const escrowContract = typeof escrowRaw === "string" && isAddress(escrowRaw) ? getAddress(escrowRaw) : channelEntry.escrowContract;
|
|
810
|
+
return new ZeroSessionCloseFailedError(message, {
|
|
811
|
+
session: {
|
|
812
|
+
channelId: channelEntry.channelId,
|
|
813
|
+
escrowContract,
|
|
814
|
+
chainId,
|
|
815
|
+
recipient,
|
|
816
|
+
cumulativeAmount: channelEntry.cumulativeAmount.toString()
|
|
817
|
+
},
|
|
818
|
+
response,
|
|
819
|
+
capturedAmount,
|
|
820
|
+
cause
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
prepareTempoFunds = async (account, challenge, maxPayUnits, displayCostAmount) => {
|
|
824
|
+
const challengeRequest = challenge.request;
|
|
825
|
+
let requiredRaw;
|
|
826
|
+
if (challenge.intent === "session") {
|
|
827
|
+
const suggested = isUintString(challengeRequest.suggestedDeposit) ? BigInt(challengeRequest.suggestedDeposit) : 0n;
|
|
828
|
+
if (suggested > 0n) {
|
|
829
|
+
requiredRaw = suggested;
|
|
830
|
+
} else if (maxPayUnits !== null) {
|
|
831
|
+
requiredRaw = maxPayUnits;
|
|
832
|
+
} else {
|
|
833
|
+
requiredRaw = parseUnits(resolveDefaultPrefund(displayCostAmount), 6);
|
|
834
|
+
}
|
|
835
|
+
} else {
|
|
836
|
+
const rawAmount = challengeRequest.amount;
|
|
837
|
+
if (typeof rawAmount !== "string" || rawAmount.length > MAX_AMOUNT_DIGITS || !UINT_RE.test(rawAmount)) {
|
|
838
|
+
throw new ZeroPaymentError(
|
|
839
|
+
`MPP charge challenge amount is not an unsigned integer string: ${JSON.stringify(rawAmount)}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
requiredRaw = BigInt(rawAmount);
|
|
843
|
+
}
|
|
844
|
+
if (maxPayUnits !== null && requiredRaw > maxPayUnits) {
|
|
845
|
+
throw new ZeroPaymentError(
|
|
846
|
+
`Payment of ${formatUnits(requiredRaw, 6)} USDC exceeds maxPay ${formatUnits(maxPayUnits, 6)}`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const capturedAmount = formatUnits(requiredRaw, 6);
|
|
850
|
+
const methodDetails = challengeRequest.methodDetails;
|
|
851
|
+
const coerced = coerceTempoChainId(
|
|
852
|
+
challengeRequest.chainId ?? methodDetails?.chainId
|
|
853
|
+
);
|
|
854
|
+
if (!coerced) {
|
|
855
|
+
throw new ZeroPaymentError(
|
|
856
|
+
`MPP challenge has missing or unsupported chainId (expected ${TEMPO_CHAIN_ID} or ${TEMPO_TESTNET_CHAIN_ID})`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const tempoBalance = await this.readUsdcBalance(
|
|
860
|
+
account.address,
|
|
861
|
+
coerced.chain
|
|
862
|
+
);
|
|
863
|
+
if (tempoBalance < requiredRaw) {
|
|
864
|
+
if (coerced.chain === "tempo-testnet") {
|
|
865
|
+
throw new ZeroPaymentError(
|
|
866
|
+
`Insufficient pathUSD on Tempo testnet: have ${formatUnits(tempoBalance, 6)}, need ${capturedAmount}. Fund via the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
await this.bridgeBaseToTempo(account, requiredRaw - tempoBalance);
|
|
870
|
+
}
|
|
871
|
+
return capturedAmount;
|
|
872
|
+
};
|
|
873
|
+
bridgeBaseToTempo = async (account, requiredAmount) => {
|
|
874
|
+
this.ensureRelayClient();
|
|
875
|
+
const baseBalance = await this.readUsdcBalance(account.address, "base");
|
|
876
|
+
const buffer = calculateBuffer(baseBalance);
|
|
877
|
+
const bridgeAmount = requiredAmount + buffer;
|
|
878
|
+
if (baseBalance < bridgeAmount) {
|
|
879
|
+
throw new ZeroPaymentError(
|
|
880
|
+
`Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer). This capability uses an MPP payment channel, which is pre-funded up front. Either fund your wallet on Base, or pass \`maxPay\` to size a smaller channel (e.g. \`maxPay: '0.05'\` for a ~5-cent channel sized for a single cheap call).`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
this.client.logger?.({
|
|
884
|
+
level: "info",
|
|
885
|
+
message: `Bridging ${formatUnits(bridgeAmount, 6)} USDC from Base to Tempo...`
|
|
886
|
+
});
|
|
887
|
+
const walletClient = createWalletClient({
|
|
888
|
+
account,
|
|
889
|
+
chain: base,
|
|
890
|
+
transport: http()
|
|
891
|
+
});
|
|
892
|
+
const quote = await getClient().actions.getQuote({
|
|
893
|
+
chainId: BASE_CHAIN_ID,
|
|
894
|
+
toChainId: TEMPO_CHAIN_ID,
|
|
895
|
+
currency: USDC_BASE,
|
|
896
|
+
toCurrency: USDC_TEMPO,
|
|
897
|
+
amount: bridgeAmount.toString(),
|
|
898
|
+
tradeType: "EXACT_INPUT",
|
|
899
|
+
user: account.address,
|
|
900
|
+
recipient: account.address,
|
|
901
|
+
// usePermit signs an EIP-712 Permit2 message — works for both
|
|
902
|
+
// account mode and session mode (via managed-account adapter).
|
|
903
|
+
options: { usePermit: true }
|
|
904
|
+
});
|
|
905
|
+
let bridgeTxHash = null;
|
|
906
|
+
await getClient().actions.execute({
|
|
907
|
+
quote,
|
|
908
|
+
wallet: adaptViemWallet(walletClient),
|
|
909
|
+
onProgress: ({ txHashes }) => {
|
|
910
|
+
if (txHashes?.length && !bridgeTxHash) {
|
|
911
|
+
bridgeTxHash = txHashes[0]?.txHash ?? null;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
return bridgeTxHash;
|
|
916
|
+
};
|
|
917
|
+
ensureRelayClient = () => {
|
|
918
|
+
if (this.relayInitialized) return;
|
|
919
|
+
createClient({
|
|
920
|
+
baseApiUrl: MAINNET_RELAY_API,
|
|
921
|
+
source: "zero-sdk",
|
|
922
|
+
// Both chains registered so post-bridge confirmation polling can
|
|
923
|
+
// resolve Tempo's RPC; omitting Tempo would hang waiting for a
|
|
924
|
+
// Tempo tx hash on the Base transport.
|
|
925
|
+
chains: [
|
|
926
|
+
convertViemChainToRelayChain(base),
|
|
927
|
+
convertViemChainToRelayChain(tempoChain)
|
|
928
|
+
]
|
|
929
|
+
});
|
|
930
|
+
this.relayInitialized = true;
|
|
931
|
+
};
|
|
932
|
+
readUsdcBalance = async (address, chain) => {
|
|
933
|
+
const { viemChain, token } = (() => {
|
|
934
|
+
switch (chain) {
|
|
935
|
+
case "base":
|
|
936
|
+
return { viemChain: base, token: USDC_BASE };
|
|
937
|
+
case "base-sepolia":
|
|
938
|
+
return {
|
|
939
|
+
viemChain: baseSepolia,
|
|
940
|
+
token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
941
|
+
};
|
|
942
|
+
case "tempo":
|
|
943
|
+
return { viemChain: tempoChain, token: USDC_TEMPO };
|
|
944
|
+
case "tempo-testnet":
|
|
945
|
+
return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
|
|
946
|
+
}
|
|
947
|
+
})();
|
|
948
|
+
const client = createPublicClient({ chain: viemChain, transport: http() });
|
|
949
|
+
return await client.readContract({
|
|
950
|
+
address: token,
|
|
951
|
+
abi: ERC20_BALANCE_ABI,
|
|
952
|
+
functionName: "balanceOf",
|
|
953
|
+
args: [address]
|
|
954
|
+
});
|
|
955
|
+
};
|
|
956
|
+
// Single normalizer — string and number chainIds resolve consistently.
|
|
957
|
+
// Delegates to exported helper so fetch.ts's orphan path agrees.
|
|
958
|
+
tempoChainLabel = (challenge) => {
|
|
959
|
+
const req = challenge?.request;
|
|
960
|
+
const methodDetails = req?.methodDetails;
|
|
961
|
+
return tempoChainLabelFromId(req?.chainId ?? methodDetails?.chainId);
|
|
962
|
+
};
|
|
963
|
+
completeMppSession = async (params) => {
|
|
964
|
+
const {
|
|
965
|
+
url,
|
|
966
|
+
method,
|
|
967
|
+
challenge,
|
|
968
|
+
channelEntry,
|
|
969
|
+
openTimeCumulative,
|
|
970
|
+
response,
|
|
971
|
+
capturedAmount,
|
|
972
|
+
mppx,
|
|
973
|
+
signal,
|
|
974
|
+
configuredFetch
|
|
975
|
+
} = params;
|
|
976
|
+
const challengeRequest = challenge.request;
|
|
977
|
+
const recipientRaw = challengeRequest.recipient;
|
|
978
|
+
const methodDetails = challengeRequest.methodDetails;
|
|
979
|
+
const escrowRaw = methodDetails?.escrowContract;
|
|
980
|
+
const recipient = typeof recipientRaw === "string" && isAddress(recipientRaw) ? getAddress(recipientRaw) : null;
|
|
981
|
+
const challengeEscrow = typeof escrowRaw === "string" && isAddress(escrowRaw) ? getAddress(escrowRaw) : null;
|
|
982
|
+
const coercedChainId = coerceTempoChainId(
|
|
983
|
+
challengeRequest.chainId ?? methodDetails?.chainId
|
|
984
|
+
);
|
|
985
|
+
const { channelId, escrowContract } = channelEntry;
|
|
986
|
+
const buildOrphanFromChannelEntry = (message) => this.buildOrphanError(
|
|
987
|
+
channelEntry,
|
|
988
|
+
challenge,
|
|
989
|
+
capturedAmount,
|
|
990
|
+
response,
|
|
991
|
+
message,
|
|
992
|
+
void 0
|
|
993
|
+
);
|
|
994
|
+
if (!recipient || !challengeEscrow) {
|
|
995
|
+
throw buildOrphanFromChannelEntry(
|
|
996
|
+
`MPP session challenge has missing or malformed recipient/escrowContract; channel ${channelId} is open and requires reconciliation.`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (!coercedChainId) {
|
|
1000
|
+
throw buildOrphanFromChannelEntry(
|
|
1001
|
+
`MPP session challenge has unsupported chainId (expected ${TEMPO_CHAIN_ID} or ${TEMPO_TESTNET_CHAIN_ID}); channel ${channelId} is open and requires reconciliation.`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
const receipt = readSessionReceipt(response);
|
|
1005
|
+
let closeAmount = pickSessionCloseAmount(receipt, openTimeCumulative);
|
|
1006
|
+
if (closeAmount === 0n && channelEntry.cumulativeAmount > 0n) {
|
|
1007
|
+
closeAmount = channelEntry.cumulativeAmount;
|
|
1008
|
+
}
|
|
1009
|
+
const reconcileAmount = receipt && isUintString(receipt.acceptedCumulative) ? receipt.acceptedCumulative : closeAmount.toString();
|
|
1010
|
+
const orphanedSession = {
|
|
1011
|
+
channelId,
|
|
1012
|
+
escrowContract,
|
|
1013
|
+
chainId: coercedChainId.id,
|
|
1014
|
+
recipient,
|
|
1015
|
+
cumulativeAmount: reconcileAmount
|
|
1016
|
+
};
|
|
1017
|
+
const closeFailed = (message, cause) => new ZeroSessionCloseFailedError(message, {
|
|
1018
|
+
session: orphanedSession,
|
|
1019
|
+
response,
|
|
1020
|
+
capturedAmount,
|
|
1021
|
+
cause
|
|
1022
|
+
});
|
|
1023
|
+
if (closeAmount === 0n) {
|
|
1024
|
+
throw closeFailed(
|
|
1025
|
+
`Refusing to sign zero close credential for channel ${channelId} \u2014 neither receipt nor channelEntry produced a nonzero cumulativeAmount. Query escrow directly to recover; check logger for an earlier 'non-bigint cumulativeAmount' warning.`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
const closeChallengeResponse = new Response("", {
|
|
1029
|
+
status: 402,
|
|
1030
|
+
headers: { "WWW-Authenticate": Challenge.serialize(challenge) }
|
|
1031
|
+
});
|
|
1032
|
+
let closeCredential;
|
|
1033
|
+
try {
|
|
1034
|
+
closeCredential = await raceWithSignal(
|
|
1035
|
+
mppx.createCredential(closeChallengeResponse, {
|
|
1036
|
+
action: "close",
|
|
1037
|
+
channelId,
|
|
1038
|
+
cumulativeAmountRaw: closeAmount.toString()
|
|
1039
|
+
}),
|
|
1040
|
+
signal,
|
|
1041
|
+
this.client
|
|
1042
|
+
);
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
throw closeFailed(
|
|
1045
|
+
`Session close credential build failed for channel ${channelId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1046
|
+
err
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
let closeResponse;
|
|
1050
|
+
try {
|
|
1051
|
+
closeResponse = await configuredFetch(url, {
|
|
1052
|
+
method: method.toUpperCase(),
|
|
1053
|
+
headers: { authorization: closeCredential },
|
|
1054
|
+
signal
|
|
1055
|
+
});
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
throw closeFailed(
|
|
1058
|
+
`Session close request failed for channel ${channelId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1059
|
+
err
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (!closeResponse.ok) {
|
|
1063
|
+
const errBody = await Promise.race([
|
|
1064
|
+
closeResponse.text().catch(() => ""),
|
|
1065
|
+
new Promise((resolve) => {
|
|
1066
|
+
const t = setTimeout(() => resolve(""), 1e3);
|
|
1067
|
+
t.unref();
|
|
1068
|
+
})
|
|
1069
|
+
]);
|
|
1070
|
+
cancelBody(closeResponse);
|
|
1071
|
+
throw closeFailed(
|
|
1072
|
+
`Seller rejected close credential (status ${closeResponse.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}) for channel ${channelId}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
const closeReceipt = readSessionReceipt(closeResponse);
|
|
1076
|
+
cancelBody(closeResponse);
|
|
1077
|
+
const rawCloseTx = closeReceipt?.txHash;
|
|
1078
|
+
const closeTxHash = typeof rawCloseTx === "string" && /^0x[0-9a-fA-F]+$/.test(rawCloseTx) ? rawCloseTx : null;
|
|
1079
|
+
return {
|
|
1080
|
+
response,
|
|
1081
|
+
payment: {
|
|
1082
|
+
protocol: "mpp",
|
|
1083
|
+
chain: this.tempoChainLabel(challenge),
|
|
1084
|
+
txHash: closeTxHash,
|
|
1085
|
+
amount: capturedAmount,
|
|
1086
|
+
asset: "USDC",
|
|
1087
|
+
session: {
|
|
1088
|
+
channelId,
|
|
1089
|
+
escrowContract,
|
|
1090
|
+
chainId: coercedChainId.id,
|
|
1091
|
+
recipient,
|
|
1092
|
+
cumulativeAmount: closeReceipt && isUintString(closeReceipt.acceptedCumulative) ? closeReceipt.acceptedCumulative : closeAmount.toString()
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
};
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// package.json
|
|
1100
|
+
var package_default = {
|
|
1101
|
+
version: "0.1.0"};
|
|
1102
|
+
|
|
1103
|
+
// src/version.ts
|
|
1104
|
+
var SDK_VERSION = package_default.version;
|
|
1105
|
+
var userWalletDtoSchema = z.object({
|
|
1106
|
+
walletAddress: z.string(),
|
|
1107
|
+
source: z.enum(["self_custody", "privy_embedded"]),
|
|
1108
|
+
isPrimary: z.boolean(),
|
|
1109
|
+
linkedAt: z.union([z.string(), z.date()]).nullable().optional(),
|
|
1110
|
+
delegationGranted: z.boolean(),
|
|
1111
|
+
signerQuorumId: z.string().nullable(),
|
|
1112
|
+
// Per-transaction USDC cap in base units (6 dp). null = unlimited, 0 = free-only.
|
|
1113
|
+
maxTransactionUsdcBaseUnits: z.number().int().nullable()
|
|
1114
|
+
});
|
|
1115
|
+
var welcomeBonusSummarySchema = z.object({
|
|
1116
|
+
status: z.string(),
|
|
1117
|
+
amountUsd: z.number()
|
|
1118
|
+
});
|
|
1119
|
+
var internalUserDtoSchema = z.object({
|
|
1120
|
+
id: z.string(),
|
|
1121
|
+
email: z.string().nullable(),
|
|
1122
|
+
createdAt: z.union([z.string(), z.date()]).optional(),
|
|
1123
|
+
lastLoginAt: z.union([z.string(), z.date()]).nullable().optional(),
|
|
1124
|
+
onboardingCompleted: z.boolean().optional(),
|
|
1125
|
+
delegationGranted: z.boolean().optional(),
|
|
1126
|
+
wallets: z.array(userWalletDtoSchema).optional(),
|
|
1127
|
+
balance: z.object({ amount: z.string(), asset: z.string() }).nullable().optional(),
|
|
1128
|
+
welcomeBonus: welcomeBonusSummarySchema.nullable().optional()
|
|
1129
|
+
});
|
|
1130
|
+
var publicUserDtoSchema = z.object({
|
|
1131
|
+
user: z.object({
|
|
1132
|
+
id: z.string(),
|
|
1133
|
+
email: z.string().nullable()
|
|
1134
|
+
}),
|
|
1135
|
+
walletAddress: z.string().nullable(),
|
|
1136
|
+
balance: z.object({ amount: z.string(), asset: z.string() }).nullable(),
|
|
1137
|
+
welcomeBonus: welcomeBonusSummarySchema.nullable()
|
|
1138
|
+
});
|
|
1139
|
+
var signResponseSchema = z.object({
|
|
1140
|
+
signature: z.string().regex(/^0x[0-9a-fA-F]+$/, "must be 0x-prefixed hex"),
|
|
1141
|
+
walletAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be 0x-prefixed 40-char hex address")
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// src/schemas/device.ts
|
|
1145
|
+
var deviceStartResponseSchema = z.object({
|
|
1146
|
+
deviceCode: z.string(),
|
|
1147
|
+
userCode: z.string(),
|
|
1148
|
+
verificationUri: z.string(),
|
|
1149
|
+
pollInterval: z.number(),
|
|
1150
|
+
expiresAt: z.number()
|
|
1151
|
+
});
|
|
1152
|
+
var devicePollResponseSchema = z.union([
|
|
1153
|
+
z.object({ error: z.literal("authorization_pending") }),
|
|
1154
|
+
z.object({ error: z.literal("expired_token") }),
|
|
1155
|
+
z.object({
|
|
1156
|
+
accessToken: z.string(),
|
|
1157
|
+
refreshToken: z.string(),
|
|
1158
|
+
expiresIn: z.number(),
|
|
1159
|
+
user: internalUserDtoSchema
|
|
1160
|
+
})
|
|
1161
|
+
]);
|
|
1162
|
+
var sessionExchangeResponseSchema = z.object({
|
|
1163
|
+
token: z.string(),
|
|
1164
|
+
expiresAt: z.number()
|
|
1165
|
+
});
|
|
1166
|
+
var refreshResponseSchema = z.object({
|
|
1167
|
+
accessToken: z.string(),
|
|
1168
|
+
refreshToken: z.string()
|
|
1169
|
+
});
|
|
1170
|
+
var tryRefreshSession = async (client, callerSignal) => {
|
|
1171
|
+
const credentials = client.getCredentials();
|
|
1172
|
+
if (credentials.kind !== "session") return false;
|
|
1173
|
+
if (!credentials.refreshToken) return false;
|
|
1174
|
+
const url = `${client.baseUrl.replace(/\/$/, "")}/v1/auth/refresh`;
|
|
1175
|
+
const timeoutSignal = AbortSignal.timeout(client.timeout);
|
|
1176
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal;
|
|
1177
|
+
let response;
|
|
1178
|
+
try {
|
|
1179
|
+
response = await client.fetchImpl(url, {
|
|
1180
|
+
method: "POST",
|
|
1181
|
+
headers: { "content-type": "application/json" },
|
|
1182
|
+
body: JSON.stringify({ refreshToken: credentials.refreshToken }),
|
|
1183
|
+
signal
|
|
1184
|
+
});
|
|
1185
|
+
} catch {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
if (!response.ok) return false;
|
|
1189
|
+
let body;
|
|
1190
|
+
try {
|
|
1191
|
+
body = await response.json();
|
|
1192
|
+
} catch {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
const parsed = refreshResponseSchema.safeParse(body);
|
|
1196
|
+
if (!parsed.success) return false;
|
|
1197
|
+
await client.applyRefreshedTokens({
|
|
1198
|
+
accessToken: parsed.data.accessToken,
|
|
1199
|
+
refreshToken: parsed.data.refreshToken
|
|
1200
|
+
});
|
|
1201
|
+
return true;
|
|
1202
|
+
};
|
|
1203
|
+
var buildSessionAuthHeaders = (accessToken) => ({
|
|
1204
|
+
authorization: `Bearer ${accessToken}`
|
|
1205
|
+
});
|
|
1206
|
+
var buildCanonicalMessage = (method, pathWithQuery, body, timestamp, nonce) => {
|
|
1207
|
+
const bodyHash = createHash("sha256").update(body ?? "").digest("hex");
|
|
1208
|
+
return `${method}:${pathWithQuery}:${bodyHash}:${timestamp}:${nonce}`;
|
|
1209
|
+
};
|
|
1210
|
+
var buildAccountAuthHeaders = async (account, method, pathWithQuery, body) => {
|
|
1211
|
+
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
1212
|
+
const nonce = crypto.randomUUID();
|
|
1213
|
+
const message = buildCanonicalMessage(
|
|
1214
|
+
method,
|
|
1215
|
+
pathWithQuery,
|
|
1216
|
+
body,
|
|
1217
|
+
timestamp,
|
|
1218
|
+
nonce
|
|
1219
|
+
);
|
|
1220
|
+
const signature = await account.signMessage({ message });
|
|
1221
|
+
return {
|
|
1222
|
+
"x-zero-address": account.address,
|
|
1223
|
+
"x-zero-timestamp": timestamp,
|
|
1224
|
+
"x-zero-nonce": nonce,
|
|
1225
|
+
"x-zero-signature": signature
|
|
1226
|
+
};
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
// src/transport/http.ts
|
|
1230
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 502, 503, 504]);
|
|
1231
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
1232
|
+
if (signal?.aborted) {
|
|
1233
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const timer = setTimeout(() => {
|
|
1237
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1238
|
+
resolve();
|
|
1239
|
+
}, ms);
|
|
1240
|
+
const onAbort = () => {
|
|
1241
|
+
clearTimeout(timer);
|
|
1242
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1243
|
+
};
|
|
1244
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1245
|
+
});
|
|
1246
|
+
var buildPathWithQuery = (path, query) => {
|
|
1247
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
1248
|
+
if (!query) return normalizedPath;
|
|
1249
|
+
const params = new URLSearchParams();
|
|
1250
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1251
|
+
if (value !== void 0) params.set(key, String(value));
|
|
1252
|
+
}
|
|
1253
|
+
const qs = params.toString();
|
|
1254
|
+
return qs ? `${normalizedPath}?${qs}` : normalizedPath;
|
|
1255
|
+
};
|
|
1256
|
+
var joinUrl = (baseUrl, pathWithQuery) => {
|
|
1257
|
+
const base3 = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
1258
|
+
return `${base3}${pathWithQuery}`;
|
|
1259
|
+
};
|
|
1260
|
+
var buildHeaders = async (client, method, pathWithQuery, hasBody, bodyStr) => {
|
|
1261
|
+
const headers = { ...client.defaultHeaders };
|
|
1262
|
+
if (hasBody) headers["content-type"] = "application/json";
|
|
1263
|
+
headers["x-zero-sdk-version"] = SDK_VERSION;
|
|
1264
|
+
if (client.apiVersion) headers["x-zero-api-version"] = client.apiVersion;
|
|
1265
|
+
const credentials = client.getCredentials();
|
|
1266
|
+
if (credentials.kind === "session") {
|
|
1267
|
+
Object.assign(headers, buildSessionAuthHeaders(credentials.accessToken));
|
|
1268
|
+
} else if (credentials.kind === "account") {
|
|
1269
|
+
const signed = await buildAccountAuthHeaders(
|
|
1270
|
+
credentials.account,
|
|
1271
|
+
method,
|
|
1272
|
+
pathWithQuery,
|
|
1273
|
+
bodyStr
|
|
1274
|
+
);
|
|
1275
|
+
Object.assign(headers, signed);
|
|
1276
|
+
}
|
|
1277
|
+
return headers;
|
|
1278
|
+
};
|
|
1279
|
+
var parseErrorBody = async (response) => {
|
|
1280
|
+
try {
|
|
1281
|
+
return await response.clone().json();
|
|
1282
|
+
} catch {
|
|
1283
|
+
try {
|
|
1284
|
+
return await response.text();
|
|
1285
|
+
} catch {
|
|
1286
|
+
return void 0;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
var errorMessageFromBody = (body, status) => {
|
|
1291
|
+
if (body !== null && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
|
1292
|
+
return body.error;
|
|
1293
|
+
}
|
|
1294
|
+
return `HTTP ${status}`;
|
|
1295
|
+
};
|
|
1296
|
+
var request = async (client, opts, schema) => {
|
|
1297
|
+
const pathWithQuery = buildPathWithQuery(opts.path, opts.query);
|
|
1298
|
+
const url = joinUrl(client.baseUrl, pathWithQuery);
|
|
1299
|
+
const hasBody = opts.body !== void 0;
|
|
1300
|
+
const bodyStr = hasBody ? JSON.stringify(
|
|
1301
|
+
opts.body,
|
|
1302
|
+
(_k, v) => typeof v === "bigint" ? v.toString() : v
|
|
1303
|
+
) : void 0;
|
|
1304
|
+
const configuredFetch = buildConfiguredFetch(client);
|
|
1305
|
+
const performOnce = async () => configuredFetch(url, {
|
|
1306
|
+
method: opts.method,
|
|
1307
|
+
headers: await buildHeaders(
|
|
1308
|
+
client,
|
|
1309
|
+
opts.method,
|
|
1310
|
+
pathWithQuery,
|
|
1311
|
+
hasBody,
|
|
1312
|
+
bodyStr
|
|
1313
|
+
),
|
|
1314
|
+
body: bodyStr,
|
|
1315
|
+
signal: opts.signal
|
|
1316
|
+
});
|
|
1317
|
+
let refreshAttempted = false;
|
|
1318
|
+
const executeWithRefresh = async () => {
|
|
1319
|
+
const response = await performOnce();
|
|
1320
|
+
if (response.status !== 401 || refreshAttempted) return response;
|
|
1321
|
+
const credentials = client.getCredentials();
|
|
1322
|
+
if (credentials.kind !== "session") return response;
|
|
1323
|
+
refreshAttempted = true;
|
|
1324
|
+
const refreshed = await client.refreshSessionOnce(opts.signal);
|
|
1325
|
+
if (!refreshed) return response;
|
|
1326
|
+
return await performOnce();
|
|
1327
|
+
};
|
|
1328
|
+
let lastNetworkError;
|
|
1329
|
+
for (let attempt = 0; attempt <= client.maxRetries; attempt++) {
|
|
1330
|
+
let response;
|
|
1331
|
+
try {
|
|
1332
|
+
response = await executeWithRefresh();
|
|
1333
|
+
} catch (e) {
|
|
1334
|
+
if (e instanceof ZeroError) throw e;
|
|
1335
|
+
lastNetworkError = e;
|
|
1336
|
+
if (attempt < client.maxRetries) {
|
|
1337
|
+
try {
|
|
1338
|
+
await sleep(2 ** attempt * 200, opts.signal);
|
|
1339
|
+
} catch (sleepErr) {
|
|
1340
|
+
throw new ZeroApiError("Request aborted by caller", {
|
|
1341
|
+
status: 0,
|
|
1342
|
+
cause: sleepErr
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
throw new ZeroApiError("Network request failed", {
|
|
1348
|
+
status: 0,
|
|
1349
|
+
cause: e
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
if (response.ok) {
|
|
1353
|
+
let text;
|
|
1354
|
+
try {
|
|
1355
|
+
text = await response.text();
|
|
1356
|
+
} catch (e) {
|
|
1357
|
+
throw new ZeroApiError("Response body read failed", {
|
|
1358
|
+
status: response.status,
|
|
1359
|
+
cause: e
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
let json;
|
|
1363
|
+
try {
|
|
1364
|
+
json = JSON.parse(text);
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
const parsed = schema.safeParse(json);
|
|
1368
|
+
if (!parsed.success) {
|
|
1369
|
+
throw new ZeroValidationError(
|
|
1370
|
+
`Response did not match expected schema for ${opts.method} ${opts.path}`,
|
|
1371
|
+
{ cause: parsed.error, body: text }
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
return parsed.data;
|
|
1375
|
+
}
|
|
1376
|
+
if (RETRYABLE_STATUS.has(response.status) && attempt < client.maxRetries) {
|
|
1377
|
+
try {
|
|
1378
|
+
await sleep(2 ** attempt * 200, opts.signal);
|
|
1379
|
+
} catch (sleepErr) {
|
|
1380
|
+
throw new ZeroApiError("Request aborted by caller", {
|
|
1381
|
+
status: 0,
|
|
1382
|
+
cause: sleepErr
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
const errorBody = await parseErrorBody(response);
|
|
1388
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
1389
|
+
throw new ZeroApiError(errorMessageFromBody(errorBody, response.status), {
|
|
1390
|
+
status: response.status,
|
|
1391
|
+
requestId,
|
|
1392
|
+
body: errorBody
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
throw new ZeroApiError("Exhausted retries", {
|
|
1396
|
+
status: 0,
|
|
1397
|
+
cause: lastNetworkError
|
|
1398
|
+
});
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// src/namespaces/auth-device.ts
|
|
1402
|
+
var AuthDevice = class {
|
|
1403
|
+
constructor(client) {
|
|
1404
|
+
this.client = client;
|
|
1405
|
+
}
|
|
1406
|
+
start = (opts = {}) => request(
|
|
1407
|
+
this.client,
|
|
1408
|
+
{
|
|
1409
|
+
method: "POST",
|
|
1410
|
+
path: "/v1/auth/device/start",
|
|
1411
|
+
signal: opts.signal
|
|
1412
|
+
},
|
|
1413
|
+
deviceStartResponseSchema
|
|
1414
|
+
);
|
|
1415
|
+
poll = async (deviceCode, opts = {}) => {
|
|
1416
|
+
const parsed = await request(
|
|
1417
|
+
this.client,
|
|
1418
|
+
{
|
|
1419
|
+
method: "POST",
|
|
1420
|
+
path: "/v1/auth/device/poll",
|
|
1421
|
+
body: { deviceCode },
|
|
1422
|
+
signal: opts.signal
|
|
1423
|
+
},
|
|
1424
|
+
devicePollResponseSchema
|
|
1425
|
+
);
|
|
1426
|
+
if ("error" in parsed) {
|
|
1427
|
+
return parsed.error === "authorization_pending" ? { status: "pending" } : { status: "expired" };
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
status: "ok",
|
|
1431
|
+
accessToken: parsed.accessToken,
|
|
1432
|
+
refreshToken: parsed.refreshToken,
|
|
1433
|
+
expiresIn: parsed.expiresIn,
|
|
1434
|
+
user: parsed.user
|
|
1435
|
+
};
|
|
1436
|
+
};
|
|
1437
|
+
};
|
|
1438
|
+
var userWalletsSchema = z.array(userWalletDtoSchema);
|
|
1439
|
+
var normalizeMessage = (message) => {
|
|
1440
|
+
if (typeof message === "string") return message;
|
|
1441
|
+
if (typeof message.raw === "string") return message.raw;
|
|
1442
|
+
return bytesToHex(message.raw);
|
|
1443
|
+
};
|
|
1444
|
+
var repackageSignFailure = (err, op) => {
|
|
1445
|
+
if (err instanceof ZeroApiError && err.status === 402) {
|
|
1446
|
+
const body = err.body;
|
|
1447
|
+
const detail = body && typeof body.error === "string" ? `: ${body.error}` : "";
|
|
1448
|
+
throw new ZeroPaymentError(
|
|
1449
|
+
`Sign endpoint refused ${op} (HTTP 402)${detail}`,
|
|
1450
|
+
{ cause: err }
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
throw err;
|
|
1454
|
+
};
|
|
1455
|
+
var Auth = class {
|
|
1456
|
+
constructor(client) {
|
|
1457
|
+
this.client = client;
|
|
1458
|
+
this.device = new AuthDevice(client);
|
|
1459
|
+
}
|
|
1460
|
+
device;
|
|
1461
|
+
// The internal (web-app facing) user profile. Richer than profile(): full
|
|
1462
|
+
// wallet list with per-wallet delegation/signer/limit metadata.
|
|
1463
|
+
me = (opts = {}) => request(
|
|
1464
|
+
this.client,
|
|
1465
|
+
{ method: "GET", path: "/v1/users/me", signal: opts.signal },
|
|
1466
|
+
internalUserDtoSchema
|
|
1467
|
+
);
|
|
1468
|
+
// The public (agent-facing) profile — identity + primary wallet address +
|
|
1469
|
+
// balance + welcome-bonus, no per-wallet metadata. Same shape the MCP
|
|
1470
|
+
// get_profile tool returns; rendered by `zero auth whoami`.
|
|
1471
|
+
profile = (opts = {}) => request(
|
|
1472
|
+
this.client,
|
|
1473
|
+
{ method: "GET", path: "/v1/users/me/profile", signal: opts.signal },
|
|
1474
|
+
publicUserDtoSchema
|
|
1475
|
+
);
|
|
1476
|
+
// The user's wallets only. Lightweight sibling of `me()` for callers that
|
|
1477
|
+
// just need wallet addresses (e.g. managed-account resolution on the pay
|
|
1478
|
+
// path) — avoids the heavier internal-profile computation behind `me()`.
|
|
1479
|
+
wallets = (opts = {}) => request(
|
|
1480
|
+
this.client,
|
|
1481
|
+
{ method: "GET", path: "/v1/users/me/wallets", signal: opts.signal },
|
|
1482
|
+
userWalletsSchema
|
|
1483
|
+
);
|
|
1484
|
+
// Exchange an ephemeral session code (e.g. runner / sandbox handoff) for
|
|
1485
|
+
// a session access token. The returned token has no refresh path — pair
|
|
1486
|
+
// with `refreshToken: ""` when constructing the client.
|
|
1487
|
+
exchangeSessionCode = (code, opts = {}) => request(
|
|
1488
|
+
this.client,
|
|
1489
|
+
{
|
|
1490
|
+
method: "POST",
|
|
1491
|
+
path: "/v1/session/exchange",
|
|
1492
|
+
body: { code },
|
|
1493
|
+
signal: opts.signal
|
|
1494
|
+
},
|
|
1495
|
+
sessionExchangeResponseSchema
|
|
1496
|
+
);
|
|
1497
|
+
// Server-side revoke of the refresh token. Throws ZeroApiError on any
|
|
1498
|
+
// non-2xx — callers that have already cleared local state (e.g. CLI
|
|
1499
|
+
// logout) typically want to swallow with `.catch(() => {})` so a flaky
|
|
1500
|
+
// server doesn't surface as a user-visible failure for a sign-out that
|
|
1501
|
+
// already succeeded locally. Response body is opaque; schema is
|
|
1502
|
+
// `z.unknown()` and the resolved value is discarded.
|
|
1503
|
+
logout = async (refreshToken, opts = {}) => {
|
|
1504
|
+
await request(
|
|
1505
|
+
this.client,
|
|
1506
|
+
{
|
|
1507
|
+
method: "POST",
|
|
1508
|
+
path: "/v1/auth/logout",
|
|
1509
|
+
body: { refreshToken },
|
|
1510
|
+
signal: opts.signal
|
|
1511
|
+
},
|
|
1512
|
+
z.unknown()
|
|
1513
|
+
);
|
|
1514
|
+
};
|
|
1515
|
+
// Force a session refresh. Routes through refreshSessionOnce() so a
|
|
1516
|
+
// proactive call doesn't race a 401-driven transport refresh —
|
|
1517
|
+
// concurrent POSTs with the same refresh token kill the session
|
|
1518
|
+
// family (RFC 6819 token-theft response).
|
|
1519
|
+
refresh = async () => {
|
|
1520
|
+
const credentials = this.client.getCredentials();
|
|
1521
|
+
if (credentials.kind !== "session") {
|
|
1522
|
+
throw new ZeroAuthError(
|
|
1523
|
+
"auth_no_credentials",
|
|
1524
|
+
"refresh() requires the client to be constructed with a session \u2014 got credentials.kind=" + credentials.kind
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
const ok = await this.client.refreshSessionOnce();
|
|
1528
|
+
if (!ok) {
|
|
1529
|
+
throw new ZeroAuthError(
|
|
1530
|
+
"auth_session_expired",
|
|
1531
|
+
"Session refresh failed. The refresh token may be revoked or expired."
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
signMessage = async (input, opts = {}) => {
|
|
1536
|
+
const credentials = this.client.getCredentials();
|
|
1537
|
+
if (credentials.kind === "account") {
|
|
1538
|
+
return await credentials.account.signMessage({ message: input.message });
|
|
1539
|
+
}
|
|
1540
|
+
if (credentials.kind === "session") {
|
|
1541
|
+
try {
|
|
1542
|
+
const parsed = await request(
|
|
1543
|
+
this.client,
|
|
1544
|
+
{
|
|
1545
|
+
method: "POST",
|
|
1546
|
+
path: "/v1/users/me/sign-message",
|
|
1547
|
+
body: { message: normalizeMessage(input.message) }
|
|
1548
|
+
},
|
|
1549
|
+
signResponseSchema
|
|
1550
|
+
);
|
|
1551
|
+
this.assertSignerMatches(parsed.walletAddress, opts.expectedAddress);
|
|
1552
|
+
return parsed.signature;
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
repackageSignFailure(err, "signMessage");
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
throw new ZeroAuthError(
|
|
1558
|
+
"auth_no_credentials",
|
|
1559
|
+
"signMessage() requires the client to be constructed with `account` or `session`."
|
|
1560
|
+
);
|
|
1561
|
+
};
|
|
1562
|
+
// Managed-only: BYO accounts sign locally via viem's account.signTransaction.
|
|
1563
|
+
signTransaction = async (input, opts = {}) => {
|
|
1564
|
+
const credentials = this.client.getCredentials();
|
|
1565
|
+
if (credentials.kind === "session") {
|
|
1566
|
+
try {
|
|
1567
|
+
const parsed = await request(
|
|
1568
|
+
this.client,
|
|
1569
|
+
{
|
|
1570
|
+
method: "POST",
|
|
1571
|
+
path: "/v1/users/me/sign-transaction",
|
|
1572
|
+
body: { unsignedTransaction: input.unsignedTransaction }
|
|
1573
|
+
},
|
|
1574
|
+
signResponseSchema
|
|
1575
|
+
);
|
|
1576
|
+
this.assertSignerMatches(parsed.walletAddress, opts.expectedAddress);
|
|
1577
|
+
return parsed.signature;
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
repackageSignFailure(err, "signTransaction");
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if (credentials.kind === "account") {
|
|
1583
|
+
throw new ZeroAuthError(
|
|
1584
|
+
"auth_error",
|
|
1585
|
+
"signTransaction() is a managed-signing operation and is not supported in account mode. Sign locally with viem's account.signTransaction instead."
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
throw new ZeroAuthError(
|
|
1589
|
+
"auth_no_credentials",
|
|
1590
|
+
"signTransaction() requires session credentials (managed signing)."
|
|
1591
|
+
);
|
|
1592
|
+
};
|
|
1593
|
+
signTypedData = async (input, opts = {}) => {
|
|
1594
|
+
const credentials = this.client.getCredentials();
|
|
1595
|
+
if (credentials.kind === "account") {
|
|
1596
|
+
if (!credentials.account.signTypedData) {
|
|
1597
|
+
throw new ZeroAuthError(
|
|
1598
|
+
"auth_error",
|
|
1599
|
+
"The configured viem account does not implement signTypedData."
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
return await credentials.account.signTypedData(input);
|
|
1603
|
+
}
|
|
1604
|
+
if (credentials.kind === "session") {
|
|
1605
|
+
try {
|
|
1606
|
+
const parsed = await request(
|
|
1607
|
+
this.client,
|
|
1608
|
+
{
|
|
1609
|
+
method: "POST",
|
|
1610
|
+
path: "/v1/users/me/sign-typed-data",
|
|
1611
|
+
body: { typedData: input }
|
|
1612
|
+
},
|
|
1613
|
+
signResponseSchema
|
|
1614
|
+
);
|
|
1615
|
+
this.assertSignerMatches(parsed.walletAddress, opts.expectedAddress);
|
|
1616
|
+
return parsed.signature;
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
repackageSignFailure(err, "signTypedData");
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
throw new ZeroAuthError(
|
|
1622
|
+
"auth_no_credentials",
|
|
1623
|
+
"signTypedData() requires the client to be constructed with `account` or `session`."
|
|
1624
|
+
);
|
|
1625
|
+
};
|
|
1626
|
+
// Compare against the caller's pinned address (not shared cache) so
|
|
1627
|
+
// concurrent pay()s can't race past each other. Mismatch → clear + throw;
|
|
1628
|
+
// the caller's retry rebuilds against the new wallet.
|
|
1629
|
+
assertSignerMatches = (serverAddr, expectedAddr) => {
|
|
1630
|
+
if (!expectedAddr) return;
|
|
1631
|
+
const canonicalServer = getAddress(serverAddr);
|
|
1632
|
+
const canonicalExpected = getAddress(expectedAddr);
|
|
1633
|
+
if (canonicalExpected === canonicalServer) return;
|
|
1634
|
+
this.client.clearManagedAccount();
|
|
1635
|
+
throw new ZeroAuthError(
|
|
1636
|
+
"auth_error",
|
|
1637
|
+
`Managed signing wallet changed (caller expected ${canonicalExpected}, server signed as ${canonicalServer}). Cache cleared \u2014 retry to rebuild against the current wallet.`
|
|
1638
|
+
);
|
|
1639
|
+
};
|
|
1640
|
+
};
|
|
1641
|
+
var BUG_REPORT_CATEGORIES = [
|
|
1642
|
+
"search_relevance",
|
|
1643
|
+
"ranking_issue",
|
|
1644
|
+
"missing_capability",
|
|
1645
|
+
"wrong_schema",
|
|
1646
|
+
"misleading_description",
|
|
1647
|
+
"broken_execution",
|
|
1648
|
+
"payment_failure",
|
|
1649
|
+
"billing_anomaly",
|
|
1650
|
+
"cli_bug",
|
|
1651
|
+
"security",
|
|
1652
|
+
"other"
|
|
1653
|
+
];
|
|
1654
|
+
var createBugReportResponseSchema = z.object({
|
|
1655
|
+
bugReportId: z.string(),
|
|
1656
|
+
status: z.string(),
|
|
1657
|
+
deduped: z.boolean(),
|
|
1658
|
+
category: z.enum(BUG_REPORT_CATEGORIES).nullable(),
|
|
1659
|
+
title: z.string().nullable(),
|
|
1660
|
+
attached: z.object({
|
|
1661
|
+
capabilityId: z.number().nullable(),
|
|
1662
|
+
runId: z.number().nullable(),
|
|
1663
|
+
searchId: z.number().nullable()
|
|
1664
|
+
})
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
// src/namespaces/bug-reports.ts
|
|
1668
|
+
var BugReports = class {
|
|
1669
|
+
constructor(client) {
|
|
1670
|
+
this.client = client;
|
|
1671
|
+
}
|
|
1672
|
+
// POST /v1/bug-reports. Wallet-attributed server-side; the SDK client's
|
|
1673
|
+
// active credential drives attribution (account-mode → EIP-191 from the
|
|
1674
|
+
// wallet, session-mode → Bearer + server resolves the user's wallet).
|
|
1675
|
+
create = (input, opts = {}) => request(
|
|
1676
|
+
this.client,
|
|
1677
|
+
{
|
|
1678
|
+
method: "POST",
|
|
1679
|
+
path: "/v1/bug-reports",
|
|
1680
|
+
body: input,
|
|
1681
|
+
signal: opts.signal
|
|
1682
|
+
},
|
|
1683
|
+
createBugReportResponseSchema
|
|
1684
|
+
);
|
|
1685
|
+
};
|
|
1686
|
+
var ratingSchema = z.object({
|
|
1687
|
+
score: z.string(),
|
|
1688
|
+
successRate: z.string(),
|
|
1689
|
+
reviews: z.number(),
|
|
1690
|
+
stars: z.string().nullable().optional(),
|
|
1691
|
+
state: z.enum(["unrated", "rated"]).optional()
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
// src/schemas/capabilities.ts
|
|
1695
|
+
var capabilityResponseSchema = z.object({
|
|
1696
|
+
uid: z.string(),
|
|
1697
|
+
slug: z.string(),
|
|
1698
|
+
name: z.string(),
|
|
1699
|
+
description: z.string(),
|
|
1700
|
+
url: z.string(),
|
|
1701
|
+
urlTemplate: z.string().nullable().optional(),
|
|
1702
|
+
method: z.string(),
|
|
1703
|
+
headers: z.record(z.string(), z.string()).nullable(),
|
|
1704
|
+
bodySchema: z.record(z.string(), z.unknown()).nullable(),
|
|
1705
|
+
responseSchema: z.record(z.string(), z.unknown()).nullable(),
|
|
1706
|
+
example: z.object({ request: z.unknown(), response: z.unknown() }).nullable(),
|
|
1707
|
+
tags: z.array(z.string()).nullable(),
|
|
1708
|
+
exampleAgentPrompt: z.string().nullable().optional(),
|
|
1709
|
+
displayCostAmount: z.string(),
|
|
1710
|
+
displayCostAsset: z.string(),
|
|
1711
|
+
reviewCount: z.number(),
|
|
1712
|
+
rating: ratingSchema,
|
|
1713
|
+
priceObserved: z.object({
|
|
1714
|
+
minCents: z.string().nullable(),
|
|
1715
|
+
medianCents: z.string().nullable(),
|
|
1716
|
+
maxCents: z.string().nullable(),
|
|
1717
|
+
// Deprecated alias for maxCents; kept for back-compat with older
|
|
1718
|
+
// API versions that still emit it.
|
|
1719
|
+
p95Cents: z.string().nullable(),
|
|
1720
|
+
sampleCount: z.number(),
|
|
1721
|
+
varies: z.boolean(),
|
|
1722
|
+
failureChargeRate: z.number().nullable()
|
|
1723
|
+
}).nullable().optional(),
|
|
1724
|
+
paymentMethods: z.array(
|
|
1725
|
+
z.object({
|
|
1726
|
+
uid: z.string(),
|
|
1727
|
+
protocol: z.string(),
|
|
1728
|
+
methodType: z.string(),
|
|
1729
|
+
chain: z.string().nullable(),
|
|
1730
|
+
mode: z.string(),
|
|
1731
|
+
costAmount: z.string(),
|
|
1732
|
+
costPer: z.string(),
|
|
1733
|
+
priority: z.number()
|
|
1734
|
+
})
|
|
1735
|
+
).nullable(),
|
|
1736
|
+
availabilityStatus: z.enum(["healthy", "unknown", "down"]).nullable().optional(),
|
|
1737
|
+
/**
|
|
1738
|
+
* @deprecated The API no longer emits `displayStatus`; it has been replaced
|
|
1739
|
+
* by the consolidated 3-value {@link capabilityResponseSchema.shape.availabilityStatus}.
|
|
1740
|
+
* Kept as an optional field for one release for back-compat (ZERO-89 soft
|
|
1741
|
+
* migration) and will be removed in the next major version.
|
|
1742
|
+
*/
|
|
1743
|
+
displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional(),
|
|
1744
|
+
activationCount: z.number().optional(),
|
|
1745
|
+
lastUsedAt: z.string().nullable().optional(),
|
|
1746
|
+
lastSuccessfullyRanAt: z.string().nullable().optional()
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
// src/namespaces/capabilities.ts
|
|
1750
|
+
var Capabilities = class {
|
|
1751
|
+
constructor(client) {
|
|
1752
|
+
this.client = client;
|
|
1753
|
+
}
|
|
1754
|
+
get = (id, opts = {}) => request(
|
|
1755
|
+
this.client,
|
|
1756
|
+
{
|
|
1757
|
+
method: "GET",
|
|
1758
|
+
path: `/v1/capabilities/${encodeURIComponent(id)}`,
|
|
1759
|
+
query: opts.searchId ? { searchId: opts.searchId } : void 0,
|
|
1760
|
+
signal: opts.signal
|
|
1761
|
+
},
|
|
1762
|
+
capabilityResponseSchema
|
|
1763
|
+
);
|
|
1764
|
+
};
|
|
1765
|
+
var createRunResponseSchema = z.object({
|
|
1766
|
+
runId: z.string()
|
|
1767
|
+
});
|
|
1768
|
+
var runListItemSchema = z.object({
|
|
1769
|
+
uid: z.string(),
|
|
1770
|
+
capabilityUid: z.string(),
|
|
1771
|
+
capabilitySlug: z.string(),
|
|
1772
|
+
capabilityName: z.string(),
|
|
1773
|
+
status: z.number().nullable(),
|
|
1774
|
+
latencyMs: z.number().nullable(),
|
|
1775
|
+
cost: z.object({ amount: z.string(), asset: z.string().nullable() }).nullable(),
|
|
1776
|
+
payment: z.object({
|
|
1777
|
+
protocol: z.string(),
|
|
1778
|
+
chain: z.string().nullable(),
|
|
1779
|
+
txHash: z.string().nullable(),
|
|
1780
|
+
mode: z.string().nullable()
|
|
1781
|
+
}).nullable(),
|
|
1782
|
+
createdAt: z.coerce.date(),
|
|
1783
|
+
reviewed: z.boolean()
|
|
1784
|
+
});
|
|
1785
|
+
var listRunsResponseSchema = z.object({
|
|
1786
|
+
runs: z.array(runListItemSchema),
|
|
1787
|
+
nextCursor: z.string().nullable()
|
|
1788
|
+
});
|
|
1789
|
+
var createReviewResponseSchema = z.object({
|
|
1790
|
+
reviewId: z.string(),
|
|
1791
|
+
recorded: z.boolean(),
|
|
1792
|
+
updated: z.boolean().optional()
|
|
1793
|
+
});
|
|
1794
|
+
var batchReviewResponseSchema = z.object({
|
|
1795
|
+
results: z.array(
|
|
1796
|
+
z.union([
|
|
1797
|
+
z.object({
|
|
1798
|
+
runId: z.string(),
|
|
1799
|
+
ok: z.literal(true),
|
|
1800
|
+
reviewId: z.string(),
|
|
1801
|
+
updated: z.boolean()
|
|
1802
|
+
}),
|
|
1803
|
+
z.object({
|
|
1804
|
+
runId: z.string(),
|
|
1805
|
+
ok: z.literal(false),
|
|
1806
|
+
status: z.number(),
|
|
1807
|
+
error: z.string()
|
|
1808
|
+
})
|
|
1809
|
+
])
|
|
1810
|
+
),
|
|
1811
|
+
summary: z.object({
|
|
1812
|
+
total: z.number(),
|
|
1813
|
+
ok: z.number(),
|
|
1814
|
+
failed: z.number()
|
|
1815
|
+
})
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
// src/namespaces/runs.ts
|
|
1819
|
+
var Runs = class {
|
|
1820
|
+
constructor(client) {
|
|
1821
|
+
this.client = client;
|
|
1822
|
+
}
|
|
1823
|
+
create = (input, opts = {}) => request(
|
|
1824
|
+
this.client,
|
|
1825
|
+
{
|
|
1826
|
+
method: "POST",
|
|
1827
|
+
path: "/v1/runs",
|
|
1828
|
+
body: input,
|
|
1829
|
+
signal: opts.signal
|
|
1830
|
+
},
|
|
1831
|
+
createRunResponseSchema
|
|
1832
|
+
);
|
|
1833
|
+
list = (params = {}, opts = {}) => {
|
|
1834
|
+
if (params.limit === 0) {
|
|
1835
|
+
if (opts.signal?.aborted) {
|
|
1836
|
+
return Promise.reject(
|
|
1837
|
+
new ZeroApiError("Request aborted by caller", { status: 0 })
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
if (params.cursor === void 0) {
|
|
1841
|
+
return Promise.reject(
|
|
1842
|
+
new ZeroApiError(
|
|
1843
|
+
"runs.list({ limit: 0 }) requires a cursor \u2014 without one the empty page is indistinguishable from end-of-stream",
|
|
1844
|
+
{ status: 0 }
|
|
1845
|
+
)
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
return Promise.resolve({
|
|
1849
|
+
runs: [],
|
|
1850
|
+
nextCursor: params.cursor
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
const query = {};
|
|
1854
|
+
if (params.capabilityId !== void 0)
|
|
1855
|
+
query.capabilityId = params.capabilityId;
|
|
1856
|
+
if (params.unreviewed !== void 0) query.unreviewed = params.unreviewed;
|
|
1857
|
+
if (params.limit !== void 0) query.limit = params.limit;
|
|
1858
|
+
if (params.cursor !== void 0) query.cursor = params.cursor;
|
|
1859
|
+
return request(
|
|
1860
|
+
this.client,
|
|
1861
|
+
{
|
|
1862
|
+
method: "GET",
|
|
1863
|
+
path: "/v1/runs",
|
|
1864
|
+
query,
|
|
1865
|
+
signal: opts.signal
|
|
1866
|
+
},
|
|
1867
|
+
listRunsResponseSchema
|
|
1868
|
+
);
|
|
1869
|
+
};
|
|
1870
|
+
review = (input, opts = {}) => request(
|
|
1871
|
+
this.client,
|
|
1872
|
+
{
|
|
1873
|
+
method: "POST",
|
|
1874
|
+
path: "/v1/reviews",
|
|
1875
|
+
body: input,
|
|
1876
|
+
signal: opts.signal
|
|
1877
|
+
},
|
|
1878
|
+
createReviewResponseSchema
|
|
1879
|
+
);
|
|
1880
|
+
// Bulk review submission. The CLI's bulk JSONL parser stays CLI-side —
|
|
1881
|
+
// SDK consumers pass already-parsed structured input.
|
|
1882
|
+
createReviewsBatch = (reviews, opts = {}) => request(
|
|
1883
|
+
this.client,
|
|
1884
|
+
{
|
|
1885
|
+
method: "POST",
|
|
1886
|
+
path: "/v1/reviews/batch",
|
|
1887
|
+
body: { reviews },
|
|
1888
|
+
signal: opts.signal
|
|
1889
|
+
},
|
|
1890
|
+
batchReviewResponseSchema
|
|
1891
|
+
);
|
|
1892
|
+
};
|
|
1893
|
+
var USDC_BASE2 = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
1894
|
+
var USDC_TEMPO2 = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
1895
|
+
var TEMPO_CHAIN_ID2 = 4217;
|
|
1896
|
+
var baseChain = base;
|
|
1897
|
+
var tempoChain2 = {
|
|
1898
|
+
id: TEMPO_CHAIN_ID2,
|
|
1899
|
+
name: "Tempo",
|
|
1900
|
+
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
1901
|
+
rpcUrls: {
|
|
1902
|
+
default: { http: ["https://rpc.tempo.xyz"] }
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
var ERC20_BALANCE_ABI2 = [
|
|
1906
|
+
{
|
|
1907
|
+
inputs: [{ name: "account", type: "address" }],
|
|
1908
|
+
name: "balanceOf",
|
|
1909
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
1910
|
+
stateMutability: "view",
|
|
1911
|
+
type: "function"
|
|
1912
|
+
}
|
|
1913
|
+
];
|
|
1914
|
+
var USDC_DECIMALS = 6;
|
|
1915
|
+
|
|
1916
|
+
// src/namespaces/wallet.ts
|
|
1917
|
+
var fundingUrlResponseSchema = z.object({ url: z.string() });
|
|
1918
|
+
var userWalletListSchema = z.array(userWalletDtoSchema);
|
|
1919
|
+
var migrateAuthorizationResponseSchema = z.object({
|
|
1920
|
+
transactionHash: z.string()
|
|
1921
|
+
});
|
|
1922
|
+
var errorMessage = (reason) => reason instanceof Error ? reason.message : String(reason);
|
|
1923
|
+
var Wallet = class {
|
|
1924
|
+
constructor(client) {
|
|
1925
|
+
this.client = client;
|
|
1926
|
+
}
|
|
1927
|
+
clients = {};
|
|
1928
|
+
// The configured wallet address, or null if the client wasn't constructed
|
|
1929
|
+
// with an account. Stable read — does not hit the network.
|
|
1930
|
+
get address() {
|
|
1931
|
+
const credentials = this.client.getCredentials();
|
|
1932
|
+
if (credentials.kind !== "account") return null;
|
|
1933
|
+
return credentials.account.address;
|
|
1934
|
+
}
|
|
1935
|
+
balance = async (opts = {}) => {
|
|
1936
|
+
const target = opts.address ?? this.address;
|
|
1937
|
+
if (!target) {
|
|
1938
|
+
throw new ZeroWalletError(
|
|
1939
|
+
"balance() requires either an `address` option or a client constructed with an account"
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
const chain = opts.chain ?? "total";
|
|
1943
|
+
if (chain !== "total") {
|
|
1944
|
+
const raw = await this.readChainBalance(chain, target);
|
|
1945
|
+
return { amount: formatUnits(raw, USDC_DECIMALS), asset: "USDC" };
|
|
1946
|
+
}
|
|
1947
|
+
const chains = ["base", "tempo"];
|
|
1948
|
+
const settled = await Promise.allSettled(
|
|
1949
|
+
chains.map((c) => this.readChainBalance(c, target))
|
|
1950
|
+
);
|
|
1951
|
+
let sum = 0n;
|
|
1952
|
+
const errors = [];
|
|
1953
|
+
for (let i = 0; i < settled.length; i++) {
|
|
1954
|
+
const result = settled[i];
|
|
1955
|
+
const chainName = chains[i];
|
|
1956
|
+
if (!result || !chainName) continue;
|
|
1957
|
+
if (result.status === "fulfilled") {
|
|
1958
|
+
sum += result.value;
|
|
1959
|
+
} else {
|
|
1960
|
+
errors.push({ chain: chainName, message: errorMessage(result.reason) });
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (errors.length === settled.length) {
|
|
1964
|
+
throw new ZeroWalletError(
|
|
1965
|
+
`balance() failed for every chain: ${errors.map((e) => `${e.chain} (${e.message})`).join(", ")}`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
const balance = {
|
|
1969
|
+
amount: formatUnits(sum, USDC_DECIMALS),
|
|
1970
|
+
asset: "USDC"
|
|
1971
|
+
};
|
|
1972
|
+
if (errors.length > 0) balance.errors = errors;
|
|
1973
|
+
return balance;
|
|
1974
|
+
};
|
|
1975
|
+
// All wallets linked to the authenticated session user. Session-only.
|
|
1976
|
+
list = (opts = {}) => request(
|
|
1977
|
+
this.client,
|
|
1978
|
+
{
|
|
1979
|
+
method: "GET",
|
|
1980
|
+
path: "/v1/users/me/wallets",
|
|
1981
|
+
signal: opts.signal
|
|
1982
|
+
},
|
|
1983
|
+
userWalletListSchema
|
|
1984
|
+
);
|
|
1985
|
+
// Provision a managed (Privy-embedded) wallet for the session user.
|
|
1986
|
+
// Idempotent server-side — repeat calls return the existing wallet.
|
|
1987
|
+
provision = (opts = {}) => request(
|
|
1988
|
+
this.client,
|
|
1989
|
+
{
|
|
1990
|
+
method: "POST",
|
|
1991
|
+
path: "/v1/users/me/wallets/provision",
|
|
1992
|
+
signal: opts.signal
|
|
1993
|
+
},
|
|
1994
|
+
userWalletDtoSchema
|
|
1995
|
+
);
|
|
1996
|
+
// Relays a BYO-wallet-signed EIP-3009 ReceiveWithAuthorization, sweeping
|
|
1997
|
+
// USDC from the BYO wallet to the user's Zero wallet on Base via the
|
|
1998
|
+
// gas-paying relayer. The SDK does not author the authorization (chain
|
|
1999
|
+
// nonce + EIP-712 domain coupling stays caller-side); it only forwards
|
|
2000
|
+
// the signed payload. Session-only.
|
|
2001
|
+
migrateAuthorization = (authorization, opts = {}) => request(
|
|
2002
|
+
this.client,
|
|
2003
|
+
{
|
|
2004
|
+
method: "POST",
|
|
2005
|
+
path: "/v1/users/me/wallets/migrate",
|
|
2006
|
+
body: { authorization },
|
|
2007
|
+
signal: opts.signal
|
|
2008
|
+
},
|
|
2009
|
+
migrateAuthorizationResponseSchema
|
|
2010
|
+
);
|
|
2011
|
+
// Routes by identity: account mode signs EIP-191 against /v1/wallet/fund-url,
|
|
2012
|
+
// session mode hits /v1/users/me/fund-url (Bearer-authed, wallet resolved
|
|
2013
|
+
// server-side). Both honor `provider` (coinbase | stripe).
|
|
2014
|
+
fundingUrl = async (opts = {}) => {
|
|
2015
|
+
const credentials = this.client.getCredentials();
|
|
2016
|
+
const path = credentials.kind === "account" ? "/v1/wallet/fund-url" : credentials.kind === "session" ? "/v1/users/me/fund-url" : null;
|
|
2017
|
+
if (!path) {
|
|
2018
|
+
throw new ZeroWalletError(
|
|
2019
|
+
"fundingUrl() requires a client constructed with an account or session credentials"
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
const query = {
|
|
2023
|
+
provider: opts.provider ?? "coinbase"
|
|
2024
|
+
};
|
|
2025
|
+
if (opts.amount) query.amount = opts.amount;
|
|
2026
|
+
const { url } = await request(
|
|
2027
|
+
this.client,
|
|
2028
|
+
{ method: "GET", path, query, signal: opts.signal },
|
|
2029
|
+
fundingUrlResponseSchema
|
|
2030
|
+
);
|
|
2031
|
+
return url;
|
|
2032
|
+
};
|
|
2033
|
+
getPublicClient = (chain) => {
|
|
2034
|
+
if (chain === "base") {
|
|
2035
|
+
if (!this.clients.base) {
|
|
2036
|
+
this.clients.base = createPublicClient({
|
|
2037
|
+
chain: baseChain,
|
|
2038
|
+
transport: http()
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
return this.clients.base;
|
|
2042
|
+
}
|
|
2043
|
+
if (!this.clients.tempo) {
|
|
2044
|
+
this.clients.tempo = createPublicClient({
|
|
2045
|
+
// biome-ignore lint/suspicious/noExplicitAny: tempo is a custom chain object that viem's stricter Chain type rejects shape-wise but accepts at runtime
|
|
2046
|
+
chain: tempoChain2,
|
|
2047
|
+
transport: http()
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
return this.clients.tempo;
|
|
2051
|
+
};
|
|
2052
|
+
/**
|
|
2053
|
+
* @internal
|
|
2054
|
+
*
|
|
2055
|
+
* Per-chain USDC balance read. Exposed (rather than private) so unit
|
|
2056
|
+
* tests can stub it without standing up a real viem public client —
|
|
2057
|
+
* viem's `http()` transport doesn't go through `client.fetchImpl`, so
|
|
2058
|
+
* the standard mock-fetch trick doesn't cover this path.
|
|
2059
|
+
*/
|
|
2060
|
+
readChainBalance = async (chain, address) => {
|
|
2061
|
+
const publicClient = this.getPublicClient(chain);
|
|
2062
|
+
const tokenAddress = chain === "base" ? USDC_BASE2 : USDC_TEMPO2;
|
|
2063
|
+
const balance = await publicClient.readContract({
|
|
2064
|
+
address: tokenAddress,
|
|
2065
|
+
abi: ERC20_BALANCE_ABI2,
|
|
2066
|
+
functionName: "balanceOf",
|
|
2067
|
+
args: [address]
|
|
2068
|
+
});
|
|
2069
|
+
return balance;
|
|
2070
|
+
};
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
// src/options.ts
|
|
2074
|
+
var DEFAULT_BASE_URL = "https://api.zero.xyz";
|
|
2075
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
2076
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
2077
|
+
|
|
2078
|
+
// src/auth/credentials.ts
|
|
2079
|
+
var noopRefresh = async () => {
|
|
2080
|
+
};
|
|
2081
|
+
var MAX_402_PROBE_BYTES = 1024 * 1024;
|
|
2082
|
+
var MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
2083
|
+
var TEXT_CONTENT_TYPES = [
|
|
2084
|
+
"application/json",
|
|
2085
|
+
"application/xml",
|
|
2086
|
+
"application/javascript",
|
|
2087
|
+
"application/ecmascript",
|
|
2088
|
+
"application/x-www-form-urlencoded",
|
|
2089
|
+
"text/",
|
|
2090
|
+
"+json",
|
|
2091
|
+
"+xml"
|
|
2092
|
+
];
|
|
2093
|
+
var MPP_SCHEME_TOKENS = /* @__PURE__ */ new Set(["payment", "mpp"]);
|
|
2094
|
+
var splitWwwAuthenticateChallenges = (header) => {
|
|
2095
|
+
const chunks = [];
|
|
2096
|
+
let buf = "";
|
|
2097
|
+
let inQuotes = false;
|
|
2098
|
+
for (let i = 0; i < header.length; i++) {
|
|
2099
|
+
const ch = header[i];
|
|
2100
|
+
if (inQuotes) {
|
|
2101
|
+
if (ch === "\\" && i + 1 < header.length) {
|
|
2102
|
+
buf += ch + header[i + 1];
|
|
2103
|
+
i++;
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2106
|
+
if (ch === '"') inQuotes = false;
|
|
2107
|
+
buf += ch;
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
if (ch === '"') {
|
|
2111
|
+
inQuotes = true;
|
|
2112
|
+
buf += ch;
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (ch === ",") {
|
|
2116
|
+
chunks.push(buf);
|
|
2117
|
+
buf = "";
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
buf += ch;
|
|
2121
|
+
}
|
|
2122
|
+
if (buf.length > 0) chunks.push(buf);
|
|
2123
|
+
return chunks;
|
|
2124
|
+
};
|
|
2125
|
+
var isTextContentType = (contentType) => {
|
|
2126
|
+
if (!contentType) return true;
|
|
2127
|
+
const lower = contentType.toLowerCase();
|
|
2128
|
+
return TEXT_CONTENT_TYPES.some((marker) => lower.includes(marker));
|
|
2129
|
+
};
|
|
2130
|
+
var redactUrl = (url) => {
|
|
2131
|
+
try {
|
|
2132
|
+
const u = new URL(url);
|
|
2133
|
+
return `${u.origin}${u.pathname}`;
|
|
2134
|
+
} catch {
|
|
2135
|
+
return "<invalid url>";
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
var ECHO_DETECT_MIN_LEN = 16;
|
|
2139
|
+
var ECHO_DETECT_MAX_SCAN = 64 * 1024;
|
|
2140
|
+
var requestBodyEchoed = (candidate, requestBody) => {
|
|
2141
|
+
if (requestBody === void 0) return false;
|
|
2142
|
+
const asString = typeof requestBody === "string" ? requestBody : new TextDecoder("utf-8", { fatal: false }).decode(
|
|
2143
|
+
requestBody.byteLength > ECHO_DETECT_MAX_SCAN ? requestBody.subarray(0, ECHO_DETECT_MAX_SCAN) : requestBody
|
|
2144
|
+
);
|
|
2145
|
+
if (asString.length < ECHO_DETECT_MIN_LEN) return false;
|
|
2146
|
+
const scanLen = Math.min(asString.length, ECHO_DETECT_MAX_SCAN);
|
|
2147
|
+
for (let i = 0; i + ECHO_DETECT_MIN_LEN <= scanLen; i++) {
|
|
2148
|
+
if (candidate.includes(asString.slice(i, i + ECHO_DETECT_MIN_LEN))) {
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return false;
|
|
2153
|
+
};
|
|
2154
|
+
var cancelBody2 = (response) => {
|
|
2155
|
+
try {
|
|
2156
|
+
response.body?.cancel().catch(() => {
|
|
2157
|
+
});
|
|
2158
|
+
} catch {
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
var detectPaymentRequirement = async (response) => {
|
|
2162
|
+
if (response.status !== 402) return null;
|
|
2163
|
+
const x402Header = response.headers.get("payment-required") ?? response.headers.get("x-payment-required");
|
|
2164
|
+
if (x402Header) {
|
|
2165
|
+
cancelBody2(response);
|
|
2166
|
+
try {
|
|
2167
|
+
const decoded = JSON.parse(
|
|
2168
|
+
Buffer.from(x402Header, "base64").toString("utf8")
|
|
2169
|
+
);
|
|
2170
|
+
return { protocol: "x402", raw: decoded };
|
|
2171
|
+
} catch {
|
|
2172
|
+
return { protocol: "x402", raw: { encoded: x402Header } };
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const declaredLen = Number.parseInt(
|
|
2176
|
+
response.headers.get("content-length") ?? "",
|
|
2177
|
+
10
|
|
2178
|
+
);
|
|
2179
|
+
const bodyFits = !Number.isFinite(declaredLen) || declaredLen <= MAX_402_PROBE_BYTES;
|
|
2180
|
+
if (bodyFits) {
|
|
2181
|
+
try {
|
|
2182
|
+
const text = await readClippedText(response, MAX_402_PROBE_BYTES);
|
|
2183
|
+
if (text) {
|
|
2184
|
+
const parsed = JSON.parse(text);
|
|
2185
|
+
if (looksLikeX402Body(parsed)) {
|
|
2186
|
+
return { protocol: "x402", raw: parsed };
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
} catch {
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
const wwwAuth = response.headers.get("www-authenticate");
|
|
2193
|
+
if (wwwAuth) {
|
|
2194
|
+
const schemes = splitWwwAuthenticateChallenges(wwwAuth).map(
|
|
2195
|
+
(challenge) => challenge.trim().split(/\s+/)[0]?.toLowerCase() ?? ""
|
|
2196
|
+
);
|
|
2197
|
+
if (schemes.some((scheme) => MPP_SCHEME_TOKENS.has(scheme))) {
|
|
2198
|
+
cancelBody2(response);
|
|
2199
|
+
return { protocol: "mpp", raw: { "www-authenticate": wwwAuth } };
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
cancelBody2(response);
|
|
2203
|
+
return { protocol: "unknown", raw: {} };
|
|
2204
|
+
};
|
|
2205
|
+
var readClippedBytes = async (response, max) => {
|
|
2206
|
+
if (response.body === null) return new Uint8Array(0);
|
|
2207
|
+
const reader = response.body.getReader();
|
|
2208
|
+
const chunks = [];
|
|
2209
|
+
let total = 0;
|
|
2210
|
+
try {
|
|
2211
|
+
while (total < max) {
|
|
2212
|
+
const { done, value } = await reader.read();
|
|
2213
|
+
if (done) break;
|
|
2214
|
+
if (!value) continue;
|
|
2215
|
+
chunks.push(value);
|
|
2216
|
+
total += value.byteLength;
|
|
2217
|
+
}
|
|
2218
|
+
} finally {
|
|
2219
|
+
try {
|
|
2220
|
+
await reader.cancel();
|
|
2221
|
+
} catch {
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const merged = new Uint8Array(Math.min(total, max));
|
|
2225
|
+
let offset = 0;
|
|
2226
|
+
for (const c of chunks) {
|
|
2227
|
+
const take = Math.min(c.byteLength, max - offset);
|
|
2228
|
+
merged.set(c.subarray(0, take), offset);
|
|
2229
|
+
offset += take;
|
|
2230
|
+
if (offset >= max) break;
|
|
2231
|
+
}
|
|
2232
|
+
return merged;
|
|
2233
|
+
};
|
|
2234
|
+
var readClippedText = async (response, max) => {
|
|
2235
|
+
const bytes = await readClippedBytes(response, max);
|
|
2236
|
+
return new TextDecoder().decode(bytes);
|
|
2237
|
+
};
|
|
2238
|
+
var parseResponseBody = async (response, max) => {
|
|
2239
|
+
const contentType = response.headers.get("content-type");
|
|
2240
|
+
const bytes = await readClippedBytes(response, max + 1);
|
|
2241
|
+
const truncated = bytes.byteLength > max;
|
|
2242
|
+
const view = truncated ? bytes.subarray(0, max) : bytes;
|
|
2243
|
+
const buf = Buffer.from(view.buffer, view.byteOffset, view.byteLength);
|
|
2244
|
+
if (!isTextContentType(contentType)) {
|
|
2245
|
+
const base64 = buf.toString("base64");
|
|
2246
|
+
return {
|
|
2247
|
+
body: base64,
|
|
2248
|
+
bodyRaw: base64,
|
|
2249
|
+
bodyEncoding: "base64",
|
|
2250
|
+
truncated
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
const text = buf.toString("utf8");
|
|
2254
|
+
if (contentType?.toLowerCase().includes("json")) {
|
|
2255
|
+
try {
|
|
2256
|
+
return { body: JSON.parse(text), bodyRaw: text, truncated };
|
|
2257
|
+
} catch {
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return { body: text, bodyRaw: text, truncated };
|
|
2261
|
+
};
|
|
2262
|
+
var UPSTREAM_ERROR_FIELDS = [
|
|
2263
|
+
"error",
|
|
2264
|
+
"message",
|
|
2265
|
+
"detail",
|
|
2266
|
+
"reason",
|
|
2267
|
+
"error_description"
|
|
2268
|
+
];
|
|
2269
|
+
var UPSTREAM_ERROR_MAX_LEN = 200;
|
|
2270
|
+
var ERROR_SNIPPET_BYTES = 500;
|
|
2271
|
+
var clampUpstreamMessage = (value) => {
|
|
2272
|
+
const trimmed = value.trim();
|
|
2273
|
+
if (!trimmed) return void 0;
|
|
2274
|
+
return trimmed.length > UPSTREAM_ERROR_MAX_LEN ? `${trimmed.slice(0, UPSTREAM_ERROR_MAX_LEN)}\u2026` : trimmed;
|
|
2275
|
+
};
|
|
2276
|
+
var tryParseJson = (text) => {
|
|
2277
|
+
try {
|
|
2278
|
+
return JSON.parse(text);
|
|
2279
|
+
} catch {
|
|
2280
|
+
return null;
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
var extractUpstreamErrorMessage = (body) => {
|
|
2284
|
+
const parsed = tryParseJson(body);
|
|
2285
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
2286
|
+
return clampUpstreamMessage(body);
|
|
2287
|
+
}
|
|
2288
|
+
const record = parsed;
|
|
2289
|
+
for (const field of UPSTREAM_ERROR_FIELDS) {
|
|
2290
|
+
const value = record[field];
|
|
2291
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2292
|
+
return clampUpstreamMessage(value);
|
|
2293
|
+
}
|
|
2294
|
+
if (value && typeof value === "object") {
|
|
2295
|
+
const nested = value.message;
|
|
2296
|
+
if (typeof nested === "string" && nested.length > 0) {
|
|
2297
|
+
return clampUpstreamMessage(nested);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
return void 0;
|
|
2302
|
+
};
|
|
2303
|
+
var buildUpstreamError = (status, bodyRaw, bodyEncoding) => {
|
|
2304
|
+
if (status >= 200 && status < 300) return void 0;
|
|
2305
|
+
if (bodyRaw === null || bodyRaw.length === 0) return void 0;
|
|
2306
|
+
if (bodyEncoding === "base64") return void 0;
|
|
2307
|
+
const snippet = bodyRaw.slice(0, ERROR_SNIPPET_BYTES);
|
|
2308
|
+
return {
|
|
2309
|
+
status,
|
|
2310
|
+
message: extractUpstreamErrorMessage(bodyRaw),
|
|
2311
|
+
snippetHash: createHash("sha256").update(snippet).digest("hex"),
|
|
2312
|
+
snippetLength: snippet.length
|
|
2313
|
+
};
|
|
2314
|
+
};
|
|
2315
|
+
var computeOutcome = (status, payment, warnings) => {
|
|
2316
|
+
if (warnings.includes(FETCH_WARNINGS.mppSessionCloseFailed))
|
|
2317
|
+
return "payment_close_failed";
|
|
2318
|
+
if (status === 402 && payment !== null) return "payment_rejected";
|
|
2319
|
+
if (status === 402) return "payment_failed";
|
|
2320
|
+
if (status >= 400) return "server_error";
|
|
2321
|
+
return "success";
|
|
2322
|
+
};
|
|
2323
|
+
var errorFetchResult = (startedAt, error, skipReason, capabilityIdRequested, outcome, status = null) => {
|
|
2324
|
+
const result = {
|
|
2325
|
+
runId: null,
|
|
2326
|
+
ok: false,
|
|
2327
|
+
status,
|
|
2328
|
+
latencyMs: Date.now() - startedAt,
|
|
2329
|
+
payment: null,
|
|
2330
|
+
outcome,
|
|
2331
|
+
body: null,
|
|
2332
|
+
bodyRaw: null,
|
|
2333
|
+
error
|
|
2334
|
+
};
|
|
2335
|
+
if (capabilityIdRequested) result.runTrackingSkipped = [skipReason];
|
|
2336
|
+
return result;
|
|
2337
|
+
};
|
|
2338
|
+
var resolveRecordingClient = (client, opts) => {
|
|
2339
|
+
const credentials = client.getCredentials();
|
|
2340
|
+
const recordingClient = opts.account ? client.withAccount(opts.account) : client;
|
|
2341
|
+
const canRecord = opts.account !== void 0 || credentials.kind === "account" || credentials.kind === "session";
|
|
2342
|
+
return { recordingClient, canRecord };
|
|
2343
|
+
};
|
|
2344
|
+
var recordErrorFetchRun = async (client, opts, result) => {
|
|
2345
|
+
if (opts.capabilityId === void 0) return;
|
|
2346
|
+
const { recordingClient, canRecord } = resolveRecordingClient(client, opts);
|
|
2347
|
+
if (!canRecord) return;
|
|
2348
|
+
try {
|
|
2349
|
+
const created = await recordingClient.runs.create({
|
|
2350
|
+
capabilityId: opts.capabilityId,
|
|
2351
|
+
...opts.searchId && { searchId: opts.searchId },
|
|
2352
|
+
...result.status !== null && { status: result.status },
|
|
2353
|
+
latencyMs: result.latencyMs,
|
|
2354
|
+
...opts.requestSchema && { requestSchema: opts.requestSchema }
|
|
2355
|
+
});
|
|
2356
|
+
result.runId = created.runId;
|
|
2357
|
+
delete result.runTrackingSkipped;
|
|
2358
|
+
} catch {
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
var clientFetch = async (client, url, opts = {}) => {
|
|
2362
|
+
const startedAt = Date.now();
|
|
2363
|
+
const capabilityIdRequested = opts.capabilityId !== void 0;
|
|
2364
|
+
const method = opts.method ?? (opts.body !== void 0 ? "POST" : "GET");
|
|
2365
|
+
const probeHeaders = opts.headers ? { ...opts.headers } : void 0;
|
|
2366
|
+
const requestInit = {
|
|
2367
|
+
method,
|
|
2368
|
+
headers: probeHeaders,
|
|
2369
|
+
body: opts.body,
|
|
2370
|
+
signal: opts.signal
|
|
2371
|
+
};
|
|
2372
|
+
const configuredFetch = buildConfiguredFetch(client, {
|
|
2373
|
+
timeoutMs: opts.timeoutMs
|
|
2374
|
+
});
|
|
2375
|
+
let response;
|
|
2376
|
+
let payment = null;
|
|
2377
|
+
const skipReasons = [];
|
|
2378
|
+
const warnings = [];
|
|
2379
|
+
try {
|
|
2380
|
+
response = await configuredFetch(url, requestInit);
|
|
2381
|
+
} catch (err) {
|
|
2382
|
+
if (err instanceof ZeroError) throw err;
|
|
2383
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
2384
|
+
const result2 = errorFetchResult(
|
|
2385
|
+
startedAt,
|
|
2386
|
+
errMessage,
|
|
2387
|
+
"fetch_failed_before_response",
|
|
2388
|
+
capabilityIdRequested,
|
|
2389
|
+
"network_error"
|
|
2390
|
+
);
|
|
2391
|
+
await recordErrorFetchRun(client, opts, result2);
|
|
2392
|
+
return result2;
|
|
2393
|
+
}
|
|
2394
|
+
if (response.status === 402) {
|
|
2395
|
+
const requirement = await detectPaymentRequirement(response);
|
|
2396
|
+
if (requirement?.protocol === "x402" || requirement?.protocol === "mpp") {
|
|
2397
|
+
const payCall = requirement.protocol === "x402" ? client.payments.pay : client.payments.payMpp;
|
|
2398
|
+
try {
|
|
2399
|
+
const paid = await payCall({
|
|
2400
|
+
url,
|
|
2401
|
+
method,
|
|
2402
|
+
// Fresh clone for the paid retry — same rationale as
|
|
2403
|
+
// the probe-side clone above.
|
|
2404
|
+
headers: opts.headers ? { ...opts.headers } : void 0,
|
|
2405
|
+
body: opts.body,
|
|
2406
|
+
maxPay: opts.maxPay,
|
|
2407
|
+
account: opts.account,
|
|
2408
|
+
signal: opts.signal,
|
|
2409
|
+
timeoutMs: opts.timeoutMs,
|
|
2410
|
+
...opts.displayCostAmount && {
|
|
2411
|
+
displayCostAmount: opts.displayCostAmount
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
response = paid.response;
|
|
2415
|
+
payment = paid.payment;
|
|
2416
|
+
if (paid.warnings) warnings.push(...paid.warnings);
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
if (err instanceof ZeroSessionCloseFailedError) {
|
|
2419
|
+
response = err.response;
|
|
2420
|
+
payment = {
|
|
2421
|
+
protocol: "mpp",
|
|
2422
|
+
chain: tempoChainLabelFromId(err.session.chainId),
|
|
2423
|
+
txHash: null,
|
|
2424
|
+
amount: err.capturedAmount,
|
|
2425
|
+
asset: "USDC",
|
|
2426
|
+
session: err.session
|
|
2427
|
+
};
|
|
2428
|
+
warnings.push(FETCH_WARNINGS.mppSessionCloseFailed);
|
|
2429
|
+
} else if (err instanceof ZeroPaymentError || err instanceof ZeroAuthError) {
|
|
2430
|
+
const result2 = errorFetchResult(
|
|
2431
|
+
startedAt,
|
|
2432
|
+
err.message,
|
|
2433
|
+
"payment_failed",
|
|
2434
|
+
capabilityIdRequested,
|
|
2435
|
+
"payment_failed",
|
|
2436
|
+
402
|
|
2437
|
+
);
|
|
2438
|
+
await recordErrorFetchRun(client, opts, result2);
|
|
2439
|
+
return result2;
|
|
2440
|
+
} else {
|
|
2441
|
+
throw err;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
} else if (requirement?.protocol === "unknown") {
|
|
2445
|
+
const result2 = errorFetchResult(
|
|
2446
|
+
startedAt,
|
|
2447
|
+
"Capability returned 402 with an unrecognized payment protocol. Neither x402 nor MPP markers were detected in the response headers or body.",
|
|
2448
|
+
"unrecognized_402_protocol",
|
|
2449
|
+
capabilityIdRequested,
|
|
2450
|
+
"payment_failed",
|
|
2451
|
+
402
|
|
2452
|
+
);
|
|
2453
|
+
await recordErrorFetchRun(client, opts, result2);
|
|
2454
|
+
return result2;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
const latencyMs = Date.now() - startedAt;
|
|
2458
|
+
const maxResponseBytes = opts.maxResponseBytes ?? MAX_RESPONSE_BYTES;
|
|
2459
|
+
let body;
|
|
2460
|
+
let bodyRaw;
|
|
2461
|
+
let bodyEncoding;
|
|
2462
|
+
try {
|
|
2463
|
+
const parsed = await parseResponseBody(response, maxResponseBytes);
|
|
2464
|
+
body = parsed.body;
|
|
2465
|
+
bodyRaw = parsed.bodyRaw;
|
|
2466
|
+
bodyEncoding = parsed.bodyEncoding;
|
|
2467
|
+
if (parsed.truncated) warnings.push(FETCH_WARNINGS.bodyTruncated);
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
2470
|
+
throw new ZeroApiError("Request aborted by caller", {
|
|
2471
|
+
status: 0,
|
|
2472
|
+
cause: err
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2476
|
+
const skips = [];
|
|
2477
|
+
const settledLocally = paymentHasAnchor(payment);
|
|
2478
|
+
if (settledLocally && response.status === 402) {
|
|
2479
|
+
warnings.push(FETCH_WARNINGS.paidButStill402NotRecorded);
|
|
2480
|
+
skips.push(FETCH_WARNINGS.paidButStill402NotRecorded);
|
|
2481
|
+
}
|
|
2482
|
+
if (payment !== null && !settledLocally) {
|
|
2483
|
+
warnings.push(FETCH_WARNINGS.paymentSettlementUnconfirmed);
|
|
2484
|
+
skips.push(FETCH_WARNINGS.paymentSettlementUnconfirmed);
|
|
2485
|
+
}
|
|
2486
|
+
if (payment !== null && payment.amount === "unknown") {
|
|
2487
|
+
warnings.push(FETCH_WARNINGS.paymentAmountUnknown);
|
|
2488
|
+
}
|
|
2489
|
+
skips.push(FETCH_SKIP_REASONS.bodyReadFailed);
|
|
2490
|
+
const result2 = {
|
|
2491
|
+
runId: null,
|
|
2492
|
+
ok: false,
|
|
2493
|
+
status: response.status,
|
|
2494
|
+
latencyMs: Date.now() - startedAt,
|
|
2495
|
+
payment,
|
|
2496
|
+
outcome: computeOutcome(response.status, payment, warnings),
|
|
2497
|
+
body: null,
|
|
2498
|
+
bodyRaw: null,
|
|
2499
|
+
error: message
|
|
2500
|
+
};
|
|
2501
|
+
if (capabilityIdRequested) result2.runTrackingSkipped = skips;
|
|
2502
|
+
if (warnings.length > 0) result2.warnings = warnings;
|
|
2503
|
+
return result2;
|
|
2504
|
+
}
|
|
2505
|
+
const settled = paymentHasAnchor(payment);
|
|
2506
|
+
const paidButStill402 = settled && response.status === 402;
|
|
2507
|
+
const settlementUnconfirmed = payment !== null && !settled;
|
|
2508
|
+
if (paidButStill402) warnings.push(FETCH_WARNINGS.paidButStill402NotRecorded);
|
|
2509
|
+
if (settlementUnconfirmed)
|
|
2510
|
+
warnings.push(FETCH_WARNINGS.paymentSettlementUnconfirmed);
|
|
2511
|
+
if (payment !== null && payment.amount === "unknown") {
|
|
2512
|
+
warnings.push(FETCH_WARNINGS.paymentAmountUnknown);
|
|
2513
|
+
}
|
|
2514
|
+
let runId = null;
|
|
2515
|
+
const closeFailed = warnings.includes(FETCH_WARNINGS.mppSessionCloseFailed);
|
|
2516
|
+
const upstreamErrorForRecord = buildUpstreamError(
|
|
2517
|
+
response.status,
|
|
2518
|
+
bodyRaw,
|
|
2519
|
+
bodyEncoding
|
|
2520
|
+
);
|
|
2521
|
+
if (capabilityIdRequested) {
|
|
2522
|
+
if (paidButStill402)
|
|
2523
|
+
skipReasons.push(FETCH_WARNINGS.paidButStill402NotRecorded);
|
|
2524
|
+
else if (closeFailed)
|
|
2525
|
+
skipReasons.push(FETCH_SKIP_REASONS.mppSessionCloseFailedNotRecorded);
|
|
2526
|
+
}
|
|
2527
|
+
if (opts.capabilityId !== void 0 && !paidButStill402 && !closeFailed) {
|
|
2528
|
+
const { recordingClient, canRecord } = resolveRecordingClient(client, opts);
|
|
2529
|
+
if (!canRecord) {
|
|
2530
|
+
skipReasons.push("no_credentials");
|
|
2531
|
+
} else {
|
|
2532
|
+
const knownAmount = payment !== null && payment.amount !== "unknown";
|
|
2533
|
+
try {
|
|
2534
|
+
let derived;
|
|
2535
|
+
if (opts.deriveRunMetadata) {
|
|
2536
|
+
try {
|
|
2537
|
+
derived = opts.deriveRunMetadata({
|
|
2538
|
+
status: response.status,
|
|
2539
|
+
ok: response.ok,
|
|
2540
|
+
bodyRaw,
|
|
2541
|
+
...bodyEncoding && { bodyEncoding }
|
|
2542
|
+
});
|
|
2543
|
+
} catch {
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const result2 = await recordingClient.runs.create({
|
|
2547
|
+
capabilityId: opts.capabilityId,
|
|
2548
|
+
searchId: opts.searchId,
|
|
2549
|
+
status: response.status,
|
|
2550
|
+
latencyMs,
|
|
2551
|
+
...opts.requestSchema && { requestSchema: opts.requestSchema },
|
|
2552
|
+
...derived?.responseSchema && {
|
|
2553
|
+
responseSchema: derived.responseSchema
|
|
2554
|
+
},
|
|
2555
|
+
...upstreamErrorForRecord && {
|
|
2556
|
+
errorSnippetHash: upstreamErrorForRecord.snippetHash,
|
|
2557
|
+
errorSnippetLength: upstreamErrorForRecord.snippetLength
|
|
2558
|
+
},
|
|
2559
|
+
...payment && {
|
|
2560
|
+
...knownAmount && {
|
|
2561
|
+
costAmount: payment.amount,
|
|
2562
|
+
costAsset: payment.asset
|
|
2563
|
+
},
|
|
2564
|
+
paymentProtocol: payment.protocol,
|
|
2565
|
+
paymentChain: payment.chain,
|
|
2566
|
+
paymentTxHash: payment.txHash ?? void 0,
|
|
2567
|
+
paymentMode: payment.session ? "session" : "charge"
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
runId = result2.runId;
|
|
2571
|
+
} catch (err) {
|
|
2572
|
+
const errorMessage2 = err instanceof Error ? err.message : String(err);
|
|
2573
|
+
const code = err instanceof ZeroError ? err.code : void 0;
|
|
2574
|
+
const skipMsg = code ? `run_create_failed:${code}: ${errorMessage2}` : `run_create_failed: ${errorMessage2}`;
|
|
2575
|
+
skipReasons.push(skipMsg);
|
|
2576
|
+
client.logger?.({
|
|
2577
|
+
level: "warn",
|
|
2578
|
+
message: "Failed to record run for fetch",
|
|
2579
|
+
meta: {
|
|
2580
|
+
url: redactUrl(url),
|
|
2581
|
+
capabilityId: opts.capabilityId,
|
|
2582
|
+
error: errorMessage2,
|
|
2583
|
+
errorCode: code
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
const result = {
|
|
2590
|
+
runId,
|
|
2591
|
+
ok: response.ok,
|
|
2592
|
+
status: response.status,
|
|
2593
|
+
latencyMs,
|
|
2594
|
+
payment,
|
|
2595
|
+
outcome: computeOutcome(response.status, payment, warnings),
|
|
2596
|
+
body,
|
|
2597
|
+
bodyRaw
|
|
2598
|
+
};
|
|
2599
|
+
if (bodyEncoding) result.bodyEncoding = bodyEncoding;
|
|
2600
|
+
if (!response.ok) {
|
|
2601
|
+
const candidate = bodyRaw !== null && bodyRaw.length > 0 && bodyEncoding !== "base64" ? bodyRaw.slice(0, 240) : null;
|
|
2602
|
+
result.error = candidate !== null && !requestBodyEchoed(candidate, opts.body) ? candidate : `HTTP ${response.status}`;
|
|
2603
|
+
if (upstreamErrorForRecord) result.upstreamError = upstreamErrorForRecord;
|
|
2604
|
+
}
|
|
2605
|
+
if (capabilityIdRequested && skipReasons.length > 0) {
|
|
2606
|
+
result.runTrackingSkipped = skipReasons;
|
|
2607
|
+
}
|
|
2608
|
+
if (warnings.length > 0) result.warnings = warnings;
|
|
2609
|
+
return result;
|
|
2610
|
+
};
|
|
2611
|
+
var searchResultSchema = z.object({
|
|
2612
|
+
id: z.string(),
|
|
2613
|
+
position: z.number(),
|
|
2614
|
+
slug: z.string(),
|
|
2615
|
+
name: z.string(),
|
|
2616
|
+
canonicalName: z.string().nullable().optional(),
|
|
2617
|
+
description: z.string(),
|
|
2618
|
+
whatItDoes: z.string().nullable().optional(),
|
|
2619
|
+
url: z.string(),
|
|
2620
|
+
urlTemplate: z.string().nullable().optional(),
|
|
2621
|
+
cost: z.object({ amount: z.string(), asset: z.string() }),
|
|
2622
|
+
reviewCount: z.number().optional(),
|
|
2623
|
+
rating: ratingSchema,
|
|
2624
|
+
availabilityStatus: z.enum(["healthy", "unknown", "down"]).nullable().optional(),
|
|
2625
|
+
/**
|
|
2626
|
+
* @deprecated The API no longer emits `displayStatus`; it has been replaced
|
|
2627
|
+
* by the consolidated 3-value {@link searchResultSchema.shape.availabilityStatus}.
|
|
2628
|
+
* Kept as an optional field for one release for back-compat (ZERO-89 soft
|
|
2629
|
+
* migration) and will be removed in the next major version.
|
|
2630
|
+
*/
|
|
2631
|
+
displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional()
|
|
2632
|
+
});
|
|
2633
|
+
var searchResponseSchema = z.object({
|
|
2634
|
+
searchId: z.string(),
|
|
2635
|
+
total: z.number().optional().default(0),
|
|
2636
|
+
offset: z.number().optional().default(0),
|
|
2637
|
+
hasMore: z.boolean().optional().default(false),
|
|
2638
|
+
capabilities: z.array(searchResultSchema)
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
// src/client.ts
|
|
2642
|
+
var normalizeCredentials = (session, account) => {
|
|
2643
|
+
if (session && account) {
|
|
2644
|
+
throw new ZeroConfigurationError(
|
|
2645
|
+
"ClientOptions accepts at most one of `session` or `account`, not both. The two auth modes are non-composable."
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
if (session) {
|
|
2649
|
+
return {
|
|
2650
|
+
kind: "session",
|
|
2651
|
+
accessToken: session.accessToken,
|
|
2652
|
+
refreshToken: session.refreshToken,
|
|
2653
|
+
onRefreshed: session.onRefreshed ?? noopRefresh
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
if (account) {
|
|
2657
|
+
return { kind: "account", account };
|
|
2658
|
+
}
|
|
2659
|
+
return { kind: "none" };
|
|
2660
|
+
};
|
|
2661
|
+
var ZeroClient = class _ZeroClient {
|
|
2662
|
+
baseUrl;
|
|
2663
|
+
timeout;
|
|
2664
|
+
maxRetries;
|
|
2665
|
+
apiVersion;
|
|
2666
|
+
defaultHeaders;
|
|
2667
|
+
fetchImpl;
|
|
2668
|
+
fetchOptions;
|
|
2669
|
+
logger;
|
|
2670
|
+
// Private — read via getCredentials(), mutate only via applyRefreshedTokens().
|
|
2671
|
+
// Keeping a single mutator funnel is what lets the transport replay
|
|
2672
|
+
// requests with a fresh access token after a 401 → refresh.
|
|
2673
|
+
credentials;
|
|
2674
|
+
// Shared in-flight refresh promise. When N concurrent requests all 401 on
|
|
2675
|
+
// an expired access token, only the first triggers the network refresh;
|
|
2676
|
+
// the rest await the same promise. Without this dedup, the first refresh
|
|
2677
|
+
// rotates the refresh token, the rest present the now-revoked token, and
|
|
2678
|
+
// the server (RFC 6819) treats that as token theft and kills the entire
|
|
2679
|
+
// session family. Cleared in finally() so the NEXT expiry can refresh
|
|
2680
|
+
// again.
|
|
2681
|
+
refreshInFlight = null;
|
|
2682
|
+
// First paid call hits /v1/users/me/wallets to find the wallet; cached after.
|
|
2683
|
+
// Gen counter handles the logout race; AC owns /me's signal so it's
|
|
2684
|
+
// decoupled from any single caller and clearManagedAccount can abort it.
|
|
2685
|
+
managedAccount = null;
|
|
2686
|
+
managedAccountInFlight = null;
|
|
2687
|
+
managedAccountAC = null;
|
|
2688
|
+
managedAccountGen = 0;
|
|
2689
|
+
// Multi-tenant servers calling `client.fetch(url, { account: tenantAccount })`
|
|
2690
|
+
// on every request would otherwise construct a new ZeroClient (with
|
|
2691
|
+
// five namespace instances) per call. Keyed by lowercased account
|
|
2692
|
+
// address so equivalent accounts hit the same sub-client.
|
|
2693
|
+
//
|
|
2694
|
+
// Shared across base + sub-clients so `base.clearAccountCache()`
|
|
2695
|
+
// reaches sub-fragments (e.g. `base.withAccount(a).withAccount(b)`).
|
|
2696
|
+
// Without sharing, those fragments leak past base.clearAccountCache().
|
|
2697
|
+
//
|
|
2698
|
+
// Lifetime = base client's lifetime. Sub-clients hold a reference back
|
|
2699
|
+
// to their account via `credentials.account`, so even a WeakMap variant
|
|
2700
|
+
// could not GC entries until the base client is dropped. Long-running
|
|
2701
|
+
// multi-tenant servers should call `clearAccountCache(account?)` on
|
|
2702
|
+
// tenant churn to release per-tenant memory.
|
|
2703
|
+
withAccountCache;
|
|
2704
|
+
// Namespace surfaces. Eagerly instantiated in the constructor because they
|
|
2705
|
+
// hold only a reference to `this` — no heavy work happens until a method
|
|
2706
|
+
// is called. Lazy getters would tree-shake better but break the discoverable
|
|
2707
|
+
// `client.<tab>` ergonomics that make resource-namespaced SDKs pleasant.
|
|
2708
|
+
auth;
|
|
2709
|
+
bugReports;
|
|
2710
|
+
capabilities;
|
|
2711
|
+
payments;
|
|
2712
|
+
runs;
|
|
2713
|
+
wallet;
|
|
2714
|
+
refreshOverride;
|
|
2715
|
+
constructor(options = {}) {
|
|
2716
|
+
this.credentials = normalizeCredentials(options.session, options.account);
|
|
2717
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
2718
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
2719
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
2720
|
+
this.apiVersion = options.apiVersion;
|
|
2721
|
+
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
2722
|
+
this.fetchImpl = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
2723
|
+
this.fetchOptions = options.fetchOptions;
|
|
2724
|
+
this.logger = options.logger;
|
|
2725
|
+
this.refreshOverride = options.refresh;
|
|
2726
|
+
this.withAccountCache = options._sharedAccountCache ?? /* @__PURE__ */ new Map();
|
|
2727
|
+
this.auth = new Auth(this);
|
|
2728
|
+
this.bugReports = new BugReports(this);
|
|
2729
|
+
this.capabilities = new Capabilities(this);
|
|
2730
|
+
this.payments = new Payments(this);
|
|
2731
|
+
this.runs = new Runs(this);
|
|
2732
|
+
this.wallet = new Wallet(this);
|
|
2733
|
+
}
|
|
2734
|
+
// Convenience factory: derives a viem account from a raw 0x-prefixed
|
|
2735
|
+
// private key and constructs a client in account mode. Equivalent to
|
|
2736
|
+
// `new ZeroClient({ account: privateKeyToAccount(pk), ...opts })`.
|
|
2737
|
+
static fromPrivateKey = (privateKey, opts = {}) => new _ZeroClient({ ...opts, account: privateKeyToAccount(privateKey) });
|
|
2738
|
+
// Convenience factory: derives a viem account from a BIP-39 mnemonic and
|
|
2739
|
+
// constructs a client in account mode. The default derivation path is
|
|
2740
|
+
// viem's default (m/44'/60'/0'/0/0).
|
|
2741
|
+
static fromMnemonic = (mnemonic, opts = {}) => new _ZeroClient({ ...opts, account: mnemonicToAccount(mnemonic) });
|
|
2742
|
+
/**
|
|
2743
|
+
* @internal
|
|
2744
|
+
*
|
|
2745
|
+
* Live credential accessor for transport + auth modules. Returns the
|
|
2746
|
+
* discriminated union (not a snapshot) so auth-dispatch always sees the
|
|
2747
|
+
* freshest access token after rotation. Exposed publicly only to bridge
|
|
2748
|
+
* the SDK's module boundary — consumers should not call this directly,
|
|
2749
|
+
* and the shape is not part of the stable surface.
|
|
2750
|
+
*/
|
|
2751
|
+
getCredentials = () => this.credentials;
|
|
2752
|
+
/**
|
|
2753
|
+
* @internal
|
|
2754
|
+
*
|
|
2755
|
+
* Single in-flight refresh helper. Concurrent callers share one
|
|
2756
|
+
* invocation. Used by the transport's 401 path; consumers should call
|
|
2757
|
+
* `client.auth.refresh()` for explicit rotation.
|
|
2758
|
+
*
|
|
2759
|
+
* Only the FIRST caller's signal participates in the in-flight
|
|
2760
|
+
* refresh — combining signals retroactively isn't possible.
|
|
2761
|
+
*/
|
|
2762
|
+
refreshSessionOnce = (signal) => {
|
|
2763
|
+
if (!this.refreshInFlight) {
|
|
2764
|
+
const refresh = this.refreshOverride ? this.runRefreshOverride(this.refreshOverride, signal) : tryRefreshSession(this, signal);
|
|
2765
|
+
this.refreshInFlight = refresh.finally(() => {
|
|
2766
|
+
this.refreshInFlight = null;
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
return this.refreshInFlight;
|
|
2770
|
+
};
|
|
2771
|
+
runRefreshOverride = async (fn, signal) => {
|
|
2772
|
+
const credentials = this.credentials;
|
|
2773
|
+
if (credentials.kind !== "session") return false;
|
|
2774
|
+
let result;
|
|
2775
|
+
try {
|
|
2776
|
+
result = await fn(signal);
|
|
2777
|
+
} catch {
|
|
2778
|
+
return false;
|
|
2779
|
+
}
|
|
2780
|
+
if (!result) return false;
|
|
2781
|
+
await this.applyRefreshedTokens(result);
|
|
2782
|
+
return true;
|
|
2783
|
+
};
|
|
2784
|
+
// The single mutation funnel for credential rotation. Updates the
|
|
2785
|
+
// in-memory tokens first, then awaits the consumer's persistence callback.
|
|
2786
|
+
// Callback errors are caught and routed to client.logger if present, but
|
|
2787
|
+
// do NOT propagate — a persistence failure is not a network error and
|
|
2788
|
+
// must not trigger the transport's retry loop. The in-memory tokens are
|
|
2789
|
+
// still considered the source of truth for the current process.
|
|
2790
|
+
applyRefreshedTokens = async (next) => {
|
|
2791
|
+
if (this.credentials.kind !== "session") return;
|
|
2792
|
+
this.credentials = {
|
|
2793
|
+
...this.credentials,
|
|
2794
|
+
accessToken: next.accessToken,
|
|
2795
|
+
refreshToken: next.refreshToken
|
|
2796
|
+
};
|
|
2797
|
+
try {
|
|
2798
|
+
await this.credentials.onRefreshed(next);
|
|
2799
|
+
} catch (error) {
|
|
2800
|
+
this.logger?.({
|
|
2801
|
+
level: "error",
|
|
2802
|
+
message: "onRefreshed callback threw \u2014 tokens rotated in-memory but consumer persistence failed. The current process will use the new tokens; a process restart may replay a stale refresh token.",
|
|
2803
|
+
meta: {
|
|
2804
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
};
|
|
2809
|
+
// Silent setter for sibling-client sync: updates in-memory tokens without
|
|
2810
|
+
// firing onRefreshed (which would recurse if both clients share a
|
|
2811
|
+
// callback). No-op outside session mode.
|
|
2812
|
+
setSessionTokens = (tokens) => {
|
|
2813
|
+
if (this.credentials.kind !== "session") return;
|
|
2814
|
+
this.credentials = {
|
|
2815
|
+
...this.credentials,
|
|
2816
|
+
accessToken: tokens.accessToken,
|
|
2817
|
+
refreshToken: tokens.refreshToken
|
|
2818
|
+
};
|
|
2819
|
+
};
|
|
2820
|
+
// Returns a new client bound to a different session, inheriting
|
|
2821
|
+
// transport config but with its own credential slot. Refreshes on the
|
|
2822
|
+
// sub-client never mutate the base — critical for multi-tenant servers
|
|
2823
|
+
// where one tenant's expired token must not cascade to others.
|
|
2824
|
+
//
|
|
2825
|
+
// withSession is a trust boundary: it does NOT share the base's
|
|
2826
|
+
// withAccount cache. Sharing would let the first sibling's transport
|
|
2827
|
+
// snapshot (headers/fetchOptions/logger) leak to every later sibling
|
|
2828
|
+
// calling withAccount(a) for the same address. withSession is also
|
|
2829
|
+
// not itself cached — sessions are typically one-per-request, and
|
|
2830
|
+
// caching by token would keep secrets in memory longer than needed.
|
|
2831
|
+
withSession = (session) => new _ZeroClient({
|
|
2832
|
+
session,
|
|
2833
|
+
baseUrl: this.baseUrl,
|
|
2834
|
+
timeout: this.timeout,
|
|
2835
|
+
maxRetries: this.maxRetries,
|
|
2836
|
+
apiVersion: this.apiVersion,
|
|
2837
|
+
defaultHeaders: { ...this.defaultHeaders },
|
|
2838
|
+
fetch: this.fetchImpl,
|
|
2839
|
+
fetchOptions: this.fetchOptions ? { ...this.fetchOptions } : void 0,
|
|
2840
|
+
logger: this.logger
|
|
2841
|
+
});
|
|
2842
|
+
// Returns a new client bound to a different viem account. Mirrors
|
|
2843
|
+
// withSession for the self-custody auth path — multi-tenant servers
|
|
2844
|
+
// holding many private keys get isolated signing state without
|
|
2845
|
+
// sharing namespace caches (e.g., the Wallet namespace's per-instance
|
|
2846
|
+
// public RPC clients).
|
|
2847
|
+
//
|
|
2848
|
+
// Sub-clients are cached per account address (see `withAccountCache`
|
|
2849
|
+
// above for the lifetime). Call `clearAccountCache()` on tenant churn.
|
|
2850
|
+
//
|
|
2851
|
+
// `defaultHeaders` and `fetchOptions` are SHALLOW SNAPSHOTS taken at
|
|
2852
|
+
// sub-client construction:
|
|
2853
|
+
// - Mutating them on a SUB-client persists across cached calls for
|
|
2854
|
+
// the same account (use `opts.headers` per call instead).
|
|
2855
|
+
// - Mutating them on the BASE post-construction does NOT reach
|
|
2856
|
+
// already-cached sub-clients. A caller that swaps an
|
|
2857
|
+
// `x-zero-api-version` header on the base for a rollout would
|
|
2858
|
+
// silently miss every cached tenant. Rebuild the base + sub-cache
|
|
2859
|
+
// to apply such changes uniformly, or pass `opts.headers` per call.
|
|
2860
|
+
// This snapshot concern is orthogonal to 401 refresh: account-mode
|
|
2861
|
+
// sub-clients have no session tokens to refresh, and session refresh
|
|
2862
|
+
// mutates `credentials` (not `defaultHeaders` / `fetchOptions`) via the
|
|
2863
|
+
// `applyRefreshedTokens` funnel.
|
|
2864
|
+
//
|
|
2865
|
+
// Cache invalidates on signer rotation (same address, new
|
|
2866
|
+
// LocalAccount object) so a rotated signer doesn't silently keep
|
|
2867
|
+
// signing as the old one. Multi-tenant servers using a KMS/vault
|
|
2868
|
+
// pattern that constructs a fresh LocalAccount per request should
|
|
2869
|
+
// hoist the LocalAccount to a per-tenant cache to preserve cache
|
|
2870
|
+
// hit rate — re-deriving the account each call defeats the cache
|
|
2871
|
+
// by design (we cannot tell from outside whether the signing
|
|
2872
|
+
// function is equivalent).
|
|
2873
|
+
withAccount = (account) => {
|
|
2874
|
+
const key = account.address.toLowerCase();
|
|
2875
|
+
const cached = this.withAccountCache.get(key);
|
|
2876
|
+
if (cached) {
|
|
2877
|
+
const cachedCreds = cached.getCredentials();
|
|
2878
|
+
if (cachedCreds.kind === "account" && cachedCreds.account === account) {
|
|
2879
|
+
return cached;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
const sub = new _ZeroClient({
|
|
2883
|
+
account,
|
|
2884
|
+
baseUrl: this.baseUrl,
|
|
2885
|
+
timeout: this.timeout,
|
|
2886
|
+
maxRetries: this.maxRetries,
|
|
2887
|
+
apiVersion: this.apiVersion,
|
|
2888
|
+
defaultHeaders: { ...this.defaultHeaders },
|
|
2889
|
+
fetch: this.fetchImpl,
|
|
2890
|
+
fetchOptions: this.fetchOptions ? { ...this.fetchOptions } : void 0,
|
|
2891
|
+
logger: this.logger,
|
|
2892
|
+
_sharedAccountCache: this.withAccountCache
|
|
2893
|
+
});
|
|
2894
|
+
this.withAccountCache.set(key, sub);
|
|
2895
|
+
return sub;
|
|
2896
|
+
};
|
|
2897
|
+
/**
|
|
2898
|
+
* Evict cached `withAccount` sub-clients. Pass an account to drop a
|
|
2899
|
+
* single tenant's entry; omit the argument to clear the entire cache.
|
|
2900
|
+
* Call on tenant churn (session expiry, deprovisioning) — without
|
|
2901
|
+
* eviction the cache grows for the base client's lifetime.
|
|
2902
|
+
*/
|
|
2903
|
+
clearAccountCache = (account) => {
|
|
2904
|
+
if (account) {
|
|
2905
|
+
this.withAccountCache.delete(account.address.toLowerCase());
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
this.withAccountCache.clear();
|
|
2909
|
+
};
|
|
2910
|
+
/** @internal — used by payments.pay() in session mode. Cached, dedupes concurrent lookups. */
|
|
2911
|
+
resolveManagedAccount = (signal) => {
|
|
2912
|
+
if (this.managedAccount) return Promise.resolve(this.managedAccount);
|
|
2913
|
+
if (this.managedAccountInFlight) {
|
|
2914
|
+
return this.raceWithSignal(this.managedAccountInFlight, signal);
|
|
2915
|
+
}
|
|
2916
|
+
if (this.credentials.kind !== "session") {
|
|
2917
|
+
return Promise.reject(
|
|
2918
|
+
new ZeroAuthError(
|
|
2919
|
+
"auth_no_credentials",
|
|
2920
|
+
"resolveManagedAccount() requires session credentials."
|
|
2921
|
+
)
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
const startedAtGen = this.managedAccountGen;
|
|
2925
|
+
const ac = new AbortController();
|
|
2926
|
+
this.managedAccountAC = ac;
|
|
2927
|
+
this.managedAccountInFlight = (async () => {
|
|
2928
|
+
const wallets = await this.auth.wallets({ signal: ac.signal });
|
|
2929
|
+
const primary = wallets.find((w) => w.isPrimary && w.source === "privy_embedded") ?? wallets.find((w) => w.source === "privy_embedded");
|
|
2930
|
+
if (!primary) {
|
|
2931
|
+
throw new ZeroAuthError(
|
|
2932
|
+
"auth_error",
|
|
2933
|
+
"Session user has no Privy-embedded wallet provisioned. Managed signing requires one. Construct the client with `account` to sign locally instead."
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
if (this.managedAccountGen !== startedAtGen) {
|
|
2937
|
+
throw new ZeroAuthError(
|
|
2938
|
+
"auth_session_expired",
|
|
2939
|
+
"Session cleared during managed-account resolution. Retry to rebuild against the current session."
|
|
2940
|
+
);
|
|
2941
|
+
}
|
|
2942
|
+
const acc = createManagedAccount(this, getAddress(primary.walletAddress));
|
|
2943
|
+
this.managedAccount = acc;
|
|
2944
|
+
return acc;
|
|
2945
|
+
})().finally(() => {
|
|
2946
|
+
if (this.managedAccountGen === startedAtGen) {
|
|
2947
|
+
this.managedAccountInFlight = null;
|
|
2948
|
+
this.managedAccountAC = null;
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
return this.raceWithSignal(this.managedAccountInFlight, signal);
|
|
2952
|
+
};
|
|
2953
|
+
// Per-caller signal gate: each caller can reject on their own abort
|
|
2954
|
+
// without cancelling the shared underlying promise.
|
|
2955
|
+
raceWithSignal = (p, signal) => {
|
|
2956
|
+
if (!signal) return p;
|
|
2957
|
+
const abortErr = (cause) => new ZeroApiError("Request aborted by caller", { status: 0, cause });
|
|
2958
|
+
if (signal.aborted)
|
|
2959
|
+
return Promise.reject(
|
|
2960
|
+
abortErr(new DOMException("Aborted", "AbortError"))
|
|
2961
|
+
);
|
|
2962
|
+
return new Promise((resolve, reject) => {
|
|
2963
|
+
const onAbort = () => reject(abortErr(new DOMException("Aborted", "AbortError")));
|
|
2964
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2965
|
+
p.then(
|
|
2966
|
+
(v) => {
|
|
2967
|
+
signal.removeEventListener("abort", onAbort);
|
|
2968
|
+
resolve(v);
|
|
2969
|
+
},
|
|
2970
|
+
(e) => {
|
|
2971
|
+
signal.removeEventListener("abort", onAbort);
|
|
2972
|
+
reject(e);
|
|
2973
|
+
}
|
|
2974
|
+
);
|
|
2975
|
+
});
|
|
2976
|
+
};
|
|
2977
|
+
/** Drop the cached managed-signing account and abort any in-flight /me lookup. */
|
|
2978
|
+
clearManagedAccount = () => {
|
|
2979
|
+
this.managedAccount = null;
|
|
2980
|
+
this.managedAccountAC?.abort();
|
|
2981
|
+
this.managedAccountInFlight = null;
|
|
2982
|
+
this.managedAccountAC = null;
|
|
2983
|
+
this.managedAccountGen++;
|
|
2984
|
+
};
|
|
2985
|
+
/** @internal — Read cached managed account without triggering resolution. */
|
|
2986
|
+
peekManagedAccount = () => this.managedAccount;
|
|
2987
|
+
// `search` is a flat method, not a namespace — it's the hot path most
|
|
2988
|
+
// consumers hit first, and Stripe/Anthropic/OpenAI all keep their primary
|
|
2989
|
+
// entry point flat for the same reason.
|
|
2990
|
+
search = (query, opts = {}) => {
|
|
2991
|
+
const { signal, ...rest } = opts;
|
|
2992
|
+
return request(
|
|
2993
|
+
this,
|
|
2994
|
+
{
|
|
2995
|
+
method: "POST",
|
|
2996
|
+
path: "/v1/search",
|
|
2997
|
+
body: { query, ...rest },
|
|
2998
|
+
signal
|
|
2999
|
+
},
|
|
3000
|
+
searchResponseSchema
|
|
3001
|
+
);
|
|
3002
|
+
};
|
|
3003
|
+
// Paid fetch — search → get → fetch is Zero's canonical loop. The hot
|
|
3004
|
+
// path is flat on the client, same rationale as `search` above.
|
|
3005
|
+
// Orchestration lives in fetch.ts so this class stays readable.
|
|
3006
|
+
fetch = (url, opts = {}) => clientFetch(this, url, opts);
|
|
3007
|
+
// No-op today. Reserved for an explicit drain/cancel handle once
|
|
3008
|
+
// long-running consumers need one — do NOT rely on this to abort
|
|
3009
|
+
// in-flight requests or flush recording. Consumers needing
|
|
3010
|
+
// shutdown semantics should manage their own AbortController.
|
|
3011
|
+
close = async () => {
|
|
3012
|
+
};
|
|
3013
|
+
};
|
|
3014
|
+
|
|
3015
|
+
export { Auth, AuthDevice, BUG_REPORT_CATEGORIES, BugReports, Capabilities, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, FETCH_SKIP_REASONS, FETCH_WARNINGS, Payments, Runs, SDK_VERSION, TEMPO_CHAIN_ID, TEMPO_TESTNET_CHAIN_ID, Wallet, ZeroApiError, ZeroAuthError, ZeroClient, ZeroConfigurationError, ZeroError, ZeroPaymentError, ZeroSessionCloseFailedError, ZeroTimeoutError, ZeroValidationError, ZeroWalletError, coerceTempoChainId, createManagedAccount, paymentHasAnchor, tempoChainLabelFromId };
|
|
3016
|
+
//# sourceMappingURL=chunk-XQEHJX2X.js.map
|
|
3017
|
+
//# sourceMappingURL=chunk-XQEHJX2X.js.map
|