@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.
@@ -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