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