@agentkv/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1263 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ACCOUNT_READ_USD: () => ACCOUNT_READ_USD,
24
+ ACCOUNT_WRITE_USD: () => ACCOUNT_WRITE_USD,
25
+ AgentKV: () => AgentKV,
26
+ AgentKVError: () => import_core2.AgentKVError,
27
+ CREDIT_VALUE_USD: () => CREDIT_VALUE_USD,
28
+ DEFAULT_MAX_OP_USD: () => DEFAULT_MAX_OP_USD,
29
+ SpendCapError: () => import_core2.SpendCapError,
30
+ VERSION: () => VERSION,
31
+ decrypt: () => decrypt,
32
+ deriveKeyMaterial: () => deriveKeyMaterial,
33
+ encrypt: () => encrypt,
34
+ generateAccountKey: () => generateAccountKey,
35
+ hashKey: () => hashKey,
36
+ isAccountKeyFormat: () => isAccountKeyFormat
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+ var import_core3 = require("@agentx402-ai/core");
40
+ var import_viem3 = require("viem");
41
+ var import_accounts = require("viem/accounts");
42
+
43
+ // src/account.ts
44
+ var import_viem = require("viem");
45
+ var AK_PREFIX = "ak_";
46
+ var AK_RANDOM_BYTES = 32;
47
+ var AK_FORMAT_RE = /^ak_[0-9a-f]{64}$/;
48
+ function generateAccountKey() {
49
+ const bytes = crypto.getRandomValues(new Uint8Array(AK_RANDOM_BYTES));
50
+ return AK_PREFIX + (0, import_viem.bytesToHex)(bytes).slice(2);
51
+ }
52
+ function isAccountKeyFormat(s) {
53
+ return typeof s === "string" && AK_FORMAT_RE.test(s);
54
+ }
55
+
56
+ // src/crypto.ts
57
+ var import_hkdf = require("@noble/hashes/hkdf");
58
+ var import_hmac = require("@noble/hashes/hmac");
59
+ var import_sha2 = require("@noble/hashes/sha2");
60
+ var import_viem2 = require("viem");
61
+ var KEY_LENGTH = 32;
62
+ var IV_LENGTH = 12;
63
+ var TAG_LENGTH = 16;
64
+ var utf8 = (s) => new TextEncoder().encode(s);
65
+ var MAGIC0 = 97;
66
+ var MAGIC1 = 107;
67
+ var ENVELOPE_VER = 1;
68
+ var SUITE_AES256GCM = 1;
69
+ var KDF_V1 = 1;
70
+ var DIGEST_SCHEME_V1 = 1;
71
+ var HEADER = Uint8Array.of(MAGIC0, MAGIC1, ENVELOPE_VER, SUITE_AES256GCM, KDF_V1);
72
+ var HEADER_LEN = HEADER.length;
73
+ function normalizeEncryptionKey(key) {
74
+ const bytes = typeof key === "string" ? (0, import_viem2.hexToBytes)(key) : key;
75
+ if (bytes.length !== KEY_LENGTH) {
76
+ throw new Error(`encryption key must be 32 bytes, got ${bytes.length}`);
77
+ }
78
+ return bytes;
79
+ }
80
+ function deriveKeyMaterial(ikm) {
81
+ return {
82
+ value: (0, import_hkdf.hkdf)(import_sha2.sha256, ikm, utf8("agentkv/v1/enc"), utf8("value"), KEY_LENGTH),
83
+ keyName: (0, import_hkdf.hkdf)(import_sha2.sha256, ikm, utf8("agentkv/v1/enc"), utf8("keyname"), KEY_LENGTH),
84
+ mac: (0, import_hkdf.hkdf)(import_sha2.sha256, ikm, utf8("agentkv/v1/mac"), utf8("lookup"), KEY_LENGTH)
85
+ };
86
+ }
87
+ function hashKey(macKey, name) {
88
+ const mac = (0, import_hmac.hmac)(import_sha2.sha256, macKey, utf8(name.normalize("NFC")));
89
+ const tagged = new Uint8Array(1 + mac.length);
90
+ tagged[0] = DIGEST_SCHEME_V1;
91
+ tagged.set(mac, 1);
92
+ return toBase64Url(tagged);
93
+ }
94
+ function toBase64(bytes) {
95
+ let binary = "";
96
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
97
+ return btoa(binary);
98
+ }
99
+ function toBase64Url(bytes) {
100
+ return toBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
101
+ }
102
+ function fromBase64(b64) {
103
+ const binary = atob(b64);
104
+ const out = new Uint8Array(binary.length);
105
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
106
+ return out;
107
+ }
108
+ function aadBytes(aad) {
109
+ if (!aad) return HEADER;
110
+ const extra = utf8(aad);
111
+ const out = new Uint8Array(HEADER_LEN + extra.length);
112
+ out.set(HEADER, 0);
113
+ out.set(extra, HEADER_LEN);
114
+ return out;
115
+ }
116
+ async function importAesKey(key) {
117
+ return crypto.subtle.importKey(
118
+ "raw",
119
+ key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength),
120
+ { name: "AES-GCM" },
121
+ false,
122
+ ["encrypt", "decrypt"]
123
+ );
124
+ }
125
+ async function gcmDecrypt(key, iv, ct, aad) {
126
+ const cryptoKey = await importAesKey(key);
127
+ const plaintext = await crypto.subtle.decrypt(
128
+ { name: "AES-GCM", iv, additionalData: aad },
129
+ cryptoKey,
130
+ ct
131
+ );
132
+ return new TextDecoder().decode(plaintext);
133
+ }
134
+ async function encrypt(key, plaintext, aad) {
135
+ const cryptoKey = await importAesKey(key);
136
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
137
+ const data = utf8(plaintext);
138
+ const ct = new Uint8Array(
139
+ await crypto.subtle.encrypt(
140
+ { name: "AES-GCM", iv, additionalData: aadBytes(aad) },
141
+ cryptoKey,
142
+ data
143
+ )
144
+ );
145
+ const packed = new Uint8Array(HEADER_LEN + iv.length + ct.length);
146
+ packed.set(HEADER, 0);
147
+ packed.set(iv, HEADER_LEN);
148
+ packed.set(ct, HEADER_LEN + iv.length);
149
+ return toBase64(packed);
150
+ }
151
+ async function decrypt(key, packed, aad) {
152
+ const bytes = fromBase64(packed);
153
+ const known = bytes.length >= HEADER_LEN + IV_LENGTH + TAG_LENGTH && bytes[0] === MAGIC0 && bytes[1] === MAGIC1 && bytes[2] === ENVELOPE_VER && bytes[3] === SUITE_AES256GCM && bytes[4] === KDF_V1;
154
+ if (!known) {
155
+ throw new Error(
156
+ `decryption failed: not a recognized AgentKV envelope (ver=${bytes[2]}, suite=${bytes[3]}, kdf=${bytes[4]}) \u2014 upgrade @agentkv/client, or the encryption key is wrong / the blob is corrupted`
157
+ );
158
+ }
159
+ try {
160
+ return await gcmDecrypt(
161
+ key,
162
+ bytes.slice(HEADER_LEN, HEADER_LEN + IV_LENGTH),
163
+ bytes.slice(HEADER_LEN + IV_LENGTH),
164
+ aadBytes(aad)
165
+ );
166
+ } catch {
167
+ throw new Error(
168
+ "decryption failed: wrong encryption key, corrupted blob, or a value served for a different key"
169
+ );
170
+ }
171
+ }
172
+
173
+ // src/payment.ts
174
+ var import_core = require("@agentx402-ai/core");
175
+
176
+ // src/types.ts
177
+ var import_core2 = require("@agentx402-ai/core");
178
+ var DEFAULT_NETWORK = "eip155:8453";
179
+
180
+ // src/index.ts
181
+ var VERSION = "0.1.0";
182
+ var V1 = "/v1";
183
+ var ENC_DERIVATION_DOMAIN = { name: "AgentKV Encryption", version: "1" };
184
+ var ENC_DERIVATION_TYPES = {
185
+ Derive: [
186
+ { name: "purpose", type: "string" },
187
+ { name: "version", type: "string" }
188
+ ]
189
+ };
190
+ var ENC_DERIVATION_MESSAGE = { purpose: "encryption-key", version: "v1" };
191
+ var CREDIT_VALUE_USD = 1e-4;
192
+ var ACCOUNT_READ_USD = 3e-4;
193
+ var ACCOUNT_WRITE_USD = 5e-4;
194
+ var DEFAULT_MAX_OP_USD = 0.05;
195
+ function toWholeAtomicUsd(amountUsd) {
196
+ if (!Number.isFinite(amountUsd)) return null;
197
+ const atomic = Math.round(amountUsd * 1e6);
198
+ if (!Number.isInteger(atomic) || atomic <= 0) return null;
199
+ if (Math.abs(amountUsd * 1e6 - atomic) > atomic * 1e-9) return null;
200
+ return atomic;
201
+ }
202
+ var AgentKV = class {
203
+ /**
204
+ * The signing wallet. `undefined` in account-key mode (a managed account has no
205
+ * wallet that can sign) — the `ak_…` bearer token is the identity instead.
206
+ */
207
+ signer;
208
+ /**
209
+ * The wallet address, the per-wallet namespace. In account-key mode there is no
210
+ * wallet, so this is the zero address (a documented sentinel: the account key —
211
+ * not an address — is the identity; the server names storage by the key's hash).
212
+ */
213
+ /** The wallet address (its namespace) in wallet/signer mode; `undefined` in account-key mode. */
214
+ address;
215
+ /** The raw `ak_…` bearer token in account-key mode; `undefined` otherwise. */
216
+ accountKey;
217
+ endpoint;
218
+ network;
219
+ maxSpendUsd;
220
+ maxSessionSpendUsd;
221
+ /** Bounded internal retries on transient failures (network error / 5xx). */
222
+ maxRetries;
223
+ timeoutMs;
224
+ fetchImpl;
225
+ _ikm;
226
+ _km;
227
+ _kmPromise;
228
+ sessionSpentUsd = 0;
229
+ // --- Discounted Prepay state (opt-in; undefined => Pay-as-you-go, unchanged) ---
230
+ prepay;
231
+ /** Top-off amount in atomic USDC units (1e6), computed once in the constructor. */
232
+ topoffAtomic = 0;
233
+ /** Account-key top-off hook (account-key mode only; validated in the constructor). */
234
+ topoffPayer;
235
+ /**
236
+ * Account-key inline x402 transport hook (account-key mode only; validated in
237
+ * the constructor). Mutually exclusive with `topoffPayer` PER OP — `topoffPayer`
238
+ * always takes precedence when both are configured (see the call sites in
239
+ * set()/get(), which gate on `!this.topoffPayer`).
240
+ */
241
+ opInlinePayer;
242
+ /** Last-known credit balance as an EXACT integer credit count (never USD floats). */
243
+ knownCredits;
244
+ /** Synchronous single-flight guard: at most one in-flight top-off at a time. */
245
+ topoffInFlight = false;
246
+ /**
247
+ * The in-flight SYNCHRONOUS top-off deposit (account mode), published so a concurrent op
248
+ * that hits a hard 402 but can't claim the single-flight can await it and retry.
249
+ */
250
+ topoffPromise;
251
+ /** Cached last `PAYMENT-REQUIRED` header — the template for a proactive single-shot. */
252
+ challengeTemplate;
253
+ constructor(opts) {
254
+ this.endpoint = opts.endpoint.replace(/\/+$/, "");
255
+ this.network = opts.network ?? DEFAULT_NETWORK;
256
+ this.maxSpendUsd = opts.maxSpendUsd;
257
+ this.maxSessionSpendUsd = opts.maxSessionSpendUsd;
258
+ this.maxRetries = Math.max(0, Math.floor(opts.retries ?? 2));
259
+ this.timeoutMs = opts.timeoutMs;
260
+ this.fetchImpl = opts.fetch;
261
+ const isAccountMode = "accountKey" in opts && opts.accountKey != null;
262
+ if (opts.topoffPayer !== void 0) {
263
+ if (typeof opts.topoffPayer !== "function") {
264
+ throw new import_core2.AgentKVError("topoffPayer must be a function", "invalid_config", 0);
265
+ }
266
+ if (!isAccountMode) {
267
+ throw new import_core2.AgentKVError(
268
+ "topoffPayer is account-key-mode only; wallet mode pays its own top-offs",
269
+ "invalid_config",
270
+ 0
271
+ );
272
+ }
273
+ if (!opts.prepay) {
274
+ throw new import_core2.AgentKVError(
275
+ "topoffPayer requires prepay ({ watermark, topoff }) to control when it fires",
276
+ "invalid_config",
277
+ 0
278
+ );
279
+ }
280
+ this.topoffPayer = opts.topoffPayer;
281
+ }
282
+ if (opts.opInlinePayer !== void 0) {
283
+ if (typeof opts.opInlinePayer !== "function") {
284
+ throw new import_core2.AgentKVError("opInlinePayer must be a function", "invalid_config", 0);
285
+ }
286
+ if (!isAccountMode) {
287
+ throw new import_core2.AgentKVError(
288
+ "opInlinePayer is account-key-mode only; wallet mode pays its own x402 challenges",
289
+ "invalid_config",
290
+ 0
291
+ );
292
+ }
293
+ this.opInlinePayer = opts.opInlinePayer;
294
+ }
295
+ if (opts.prepay) {
296
+ if (isAccountMode && !opts.topoffPayer) {
297
+ throw new import_core2.AgentKVError(
298
+ "prepay in account-key mode requires a topoffPayer hook (or fund via fundAccount() / 'agentkv fund')",
299
+ "invalid_config",
300
+ 0
301
+ );
302
+ }
303
+ const topoffAtomic = toWholeAtomicUsd(opts.prepay.topoff);
304
+ if (topoffAtomic === null || !(opts.prepay.topoff >= 1)) {
305
+ throw new import_core2.AgentKVError(
306
+ "prepay.topoff must be >= $1 (a whole number of atomic USDC units)",
307
+ "invalid_config",
308
+ 0
309
+ );
310
+ }
311
+ if (!(opts.prepay.watermark >= 0)) {
312
+ throw new import_core2.AgentKVError("prepay.watermark must be >= 0", "invalid_config", 0);
313
+ }
314
+ this.prepay = opts.prepay;
315
+ this.topoffAtomic = topoffAtomic;
316
+ }
317
+ if (isAccountMode) {
318
+ if (!isAccountKeyFormat(opts.accountKey)) {
319
+ throw new import_core2.AgentKVError(
320
+ "accountKey must be a string of the form ak_<64 lowercase hex>",
321
+ "invalid_config",
322
+ 0
323
+ );
324
+ }
325
+ if (!opts.encryptionKey) {
326
+ throw new import_core2.AgentKVError(
327
+ "account-key mode requires an explicit encryptionKey (there is no wallet to derive one from)",
328
+ "invalid_config",
329
+ 0
330
+ );
331
+ }
332
+ this.accountKey = opts.accountKey;
333
+ this.signer = void 0;
334
+ this.address = void 0;
335
+ this._ikm = normalizeEncryptionKey(opts.encryptionKey);
336
+ } else if ("privateKey" in opts && opts.privateKey != null) {
337
+ if ("encryptionKey" in opts && opts.encryptionKey) {
338
+ throw new import_core2.AgentKVError(
339
+ "privateKey mode derives its encryption key from the wallet key; do not also pass encryptionKey \u2014 use { signer, encryptionKey } for an explicit AES key",
340
+ "invalid_config",
341
+ 0
342
+ );
343
+ }
344
+ this.signer = (0, import_accounts.privateKeyToAccount)(opts.privateKey);
345
+ this._ikm = (0, import_viem3.hexToBytes)(opts.privateKey);
346
+ this.address = this.signer.address;
347
+ } else if ("signer" in opts && opts.signer != null) {
348
+ this.signer = opts.signer;
349
+ if ("encryptionKey" in opts && opts.encryptionKey) {
350
+ this._ikm = normalizeEncryptionKey(opts.encryptionKey);
351
+ }
352
+ this.address = this.signer.address;
353
+ } else {
354
+ throw new import_core2.AgentKVError(
355
+ "invalid auth config: provide one of { privateKey } | { signer } | { accountKey, encryptionKey }",
356
+ "invalid_config",
357
+ 0
358
+ );
359
+ }
360
+ }
361
+ /**
362
+ * Resolve (and memoize) the AES key. Async only for the sign-to-derive shape
363
+ * (`{signer}` with no encryptionKey): the key is `HKDF` over a fixed-message
364
+ * signature, which is stable ONLY for deterministic ECDSA signers (local keys /
365
+ * RFC-6979). Non-deterministic signers (some MPC/threshold backends) would
366
+ * derive a different key each run and fail to decrypt — those must pass an
367
+ * explicit `encryptionKey`.
368
+ */
369
+ getKeyMaterial() {
370
+ if (this._km) return Promise.resolve(this._km);
371
+ if (!this._kmPromise) {
372
+ this._kmPromise = (async () => {
373
+ let ikm = this._ikm;
374
+ if (!ikm) {
375
+ if (!this.signer) {
376
+ throw new import_core2.AgentKVError(
377
+ "no encryption key material: account-key mode requires an explicit encryptionKey",
378
+ "invalid_config",
379
+ 0
380
+ );
381
+ }
382
+ const sig = await this.signer.signTypedData({
383
+ domain: ENC_DERIVATION_DOMAIN,
384
+ types: ENC_DERIVATION_TYPES,
385
+ primaryType: "Derive",
386
+ message: ENC_DERIVATION_MESSAGE
387
+ });
388
+ const sigBytes = (0, import_viem3.hexToBytes)(sig);
389
+ if (sigBytes.length !== 65) {
390
+ throw new import_core2.AgentKVError(
391
+ `sign-to-derive expected a 65-byte EIP-712 signature but got ${sigBytes.length} bytes; this signer's format is unstable for key derivation \u2014 construct with an explicit encryptionKey`,
392
+ "invalid_config",
393
+ 0
394
+ );
395
+ }
396
+ ikm = sigBytes;
397
+ }
398
+ const km = deriveKeyMaterial(ikm);
399
+ this._km = km;
400
+ return km;
401
+ })().catch((err) => {
402
+ this._kmPromise = void 0;
403
+ throw err;
404
+ });
405
+ }
406
+ return this._kmPromise;
407
+ }
408
+ /**
409
+ * Decrypt a value envelope with the current value key, binding the key's blind-index
410
+ * digest into the AAD so a value the server serves for the wrong key fails the auth tag.
411
+ */
412
+ async decryptValue(packed, key) {
413
+ const km = await this.getKeyMaterial();
414
+ return decrypt(km.value, packed, hashKey(km.mac, key));
415
+ }
416
+ assertSpend(usd, opts = {}) {
417
+ if (!opts.bypassPerOpCap && this.maxSpendUsd !== void 0 && usd > this.maxSpendUsd) {
418
+ throw new import_core2.SpendCapError(`spend $${usd} exceeds per-call cap $${this.maxSpendUsd}`);
419
+ }
420
+ if (this.maxSessionSpendUsd !== void 0 && this.sessionSpentUsd + usd > this.maxSessionSpendUsd) {
421
+ throw new import_core2.SpendCapError(
422
+ `spend $${usd} would exceed session cap $${this.maxSessionSpendUsd} (spent $${this.sessionSpentUsd})`
423
+ );
424
+ }
425
+ }
426
+ recordSpend(usd) {
427
+ this.sessionSpentUsd += usd;
428
+ }
429
+ /**
430
+ * Reject a SERVER-QUOTED per-op price above a sane ceiling in the DEFAULT (cap-less) config.
431
+ * When `maxSpendUsd` is set, `assertSpend` already bounds the op price; when it is NOT set,
432
+ * a compromised or spoofed worker could otherwise answer a routine $0.005 read with a 402
433
+ * challenge for the wallet's whole balance and the client would sign the EIP-3009
434
+ * authorization. Callers who genuinely need a pricier op opt in via `maxSpendUsd`.
435
+ */
436
+ assertOpPriceCeiling(usd) {
437
+ if (this.maxSpendUsd === void 0 && usd > DEFAULT_MAX_OP_USD) {
438
+ throw new import_core2.SpendCapError(
439
+ `server-quoted op price $${usd} exceeds the built-in $${DEFAULT_MAX_OP_USD} op ceiling; set maxSpendUsd to allow a higher per-op charge`
440
+ );
441
+ }
442
+ }
443
+ /**
444
+ * The effective per-op ceiling (USD) for an inline-payer op: the caller's `maxSpendUsd`
445
+ * when set (they opted into that bound), else the built-in default ceiling. Handed to the
446
+ * hook as its hard `maxAmountAtomic`, and pre-reserved against the session cap before paying.
447
+ */
448
+ inlineOpCeilingUsd() {
449
+ return this.maxSpendUsd ?? DEFAULT_MAX_OP_USD;
450
+ }
451
+ /**
452
+ * The on-chain settlement txHash from a response's PAYMENT-RESPONSE header, or ""
453
+ * when the server served the op from existing credits (so the attached top-off
454
+ * authorization was NEVER settled — it just expires) or the header is absent. The
455
+ * worker emits PAYMENT-RESPONSE = base64(JSON `{ success, payer, amount, txHash }`)
456
+ * on any paid 200, with `txHash: ""` on the credit hot path. A proactive single-shot
457
+ * top-off must only count toward session spend when this is non-empty — otherwise no
458
+ * USDC moved and recording it inflates sessionSpentUsd, prematurely tripping the cap.
459
+ *
460
+ * Accepted trade-off: in a doubly-rare crash window (the server's settle mined on-chain
461
+ * but its ledger row was lost, AND the response was lost so the client retries), the
462
+ * worker's already-used-authorization recovery returns success with txHash "" even
463
+ * though USDC did move. The client then under-counts that top-off by one, making the
464
+ * local session cap marginally lenient — no funds are lost (the amount is still minted
465
+ * as credits). This is unavoidable (the worker cannot distinguish that case from a
466
+ * plain credit-served op) and far cheaper than the systematic L3 over-count it replaces.
467
+ */
468
+ settledTxHash(res) {
469
+ const header = res.headers.get("PAYMENT-RESPONSE");
470
+ if (!header) return "";
471
+ try {
472
+ const parsed = JSON.parse((0, import_core.decodeBase64Utf8)(header));
473
+ return typeof parsed.txHash === "string" ? parsed.txHash : "";
474
+ } catch {
475
+ return "";
476
+ }
477
+ }
478
+ /**
479
+ * Issue a request with bounded internal retry on TRANSIENT failures only: a
480
+ * thrown fetch (network error / lost response) or a 5xx. `build()` is re-invoked
481
+ * per attempt so the credit path can re-sign identity with a FRESH nonce each
482
+ * time, while the op's stable Idempotency-Key (and pinned EIP-3009 nonce on paid
483
+ * ops) makes a retry of an already-processed request dedupe server-side — so a
484
+ * lost response that the server already charged is recovered without a second
485
+ * charge. NOT retried: any 2xx/4xx (incl. the 402 credit->pay handoff, 401, 404)
486
+ * — those are returned as-is for the caller's normal handling. The bound is kept
487
+ * small so a re-sent paid authorization cannot outlive its validBefore.
488
+ *
489
+ * The retry MECHANICS (transient-status detection, backoff,
490
+ * Retry-After honoring) were extracted to `@agentx402-ai/core`'s `fetchWithRetry`
491
+ * as a pure function parameterized by `maxRetries` — this method is now a
492
+ * thin delegating wrapper so every existing `this.fetchWithRetry(...)` call
493
+ * site above is unchanged.
494
+ */
495
+ fetchWithRetry(url, build) {
496
+ return (0, import_core3.fetchWithRetry)(url, build, this.maxRetries, {
497
+ timeoutMs: this.timeoutMs,
498
+ fetchImpl: this.fetchImpl
499
+ });
500
+ }
501
+ // --- Discounted Prepay helpers -------------------------------------------
502
+ /**
503
+ * Update prepay tracking from any server response. Reads the exact integer
504
+ * credit balance (`X-AgentKV-Credits-Remaining`) and caches the most recent
505
+ * `PAYMENT-REQUIRED` challenge as the proactive single-shot template (so a
506
+ * top-off needs no preflight request). Safe to call when prepay is disabled.
507
+ */
508
+ trackBalance(res) {
509
+ const credits = res.headers.get("X-AgentKV-Credits-Remaining");
510
+ if (credits !== null && credits !== "") {
511
+ const n = Number(credits);
512
+ if (Number.isFinite(n)) this.knownCredits = n;
513
+ }
514
+ const challenge = res.headers.get("PAYMENT-REQUIRED");
515
+ if (challenge) this.challengeTemplate = challenge;
516
+ }
517
+ /** Watermark (USD) expressed in EXACT integer credits (1 credit = CREDIT_VALUE_USD = $0.0001,
518
+ * so $1 = 10,000 credits — matching the worker's mint rate). */
519
+ watermarkCredits() {
520
+ return Math.round(this.prepay.watermark / CREDIT_VALUE_USD);
521
+ }
522
+ /**
523
+ * Synchronous single-flight claim for a proactive top-off. CRITICAL: there is
524
+ * NO `await` between the watermark check and setting `topoffInFlight = true`,
525
+ * and this must be called at the very top of set/get before any await.
526
+ * Otherwise two concurrent ops both pass the check and each fire a separate
527
+ * top-off with distinct fresh nonces the server can't dedupe (double charge).
528
+ * Returns true for exactly one concurrent op below the watermark; the caller
529
+ * MUST clear `topoffInFlight` in a `finally`. Losers take the identity path.
530
+ */
531
+ tryClaimTopoff() {
532
+ if (!this.prepay || this.prepay.async || this.topoffInFlight || this.knownCredits === void 0 || this.knownCredits >= this.watermarkCredits()) {
533
+ return false;
534
+ }
535
+ this.topoffInFlight = true;
536
+ return true;
537
+ }
538
+ /**
539
+ * Single-flight claim at a hard 402 (insufficient credits). Unlike
540
+ * `tryClaimTopoff` this ignores the watermark — a 402 already proves credits
541
+ * are short — but still claims the flag synchronously so concurrent 402s don't
542
+ * each fire a top-off. Returns true only if the flag was free; the caller MUST
543
+ * clear it in a `finally`. In `async` mode we leave the 402 to be paid at the
544
+ * op price (the background deposit replenishes credits separately).
545
+ */
546
+ tryClaimTopoffOnFault() {
547
+ if (!this.prepay || this.prepay.async || this.topoffInFlight) return false;
548
+ this.topoffInFlight = true;
549
+ return true;
550
+ }
551
+ /**
552
+ * Run a synchronous account-mode top-off while PUBLISHING its in-flight promise, so a
553
+ * concurrent op that hits a hard 402 (and can't claim the single-flight) can await THIS
554
+ * deposit and retry rather than surfacing the 402. The caller holds the single-flight claim.
555
+ */
556
+ async runSharedTopoff() {
557
+ const p = this.runAccountTopoff();
558
+ this.topoffPromise = p;
559
+ try {
560
+ await p;
561
+ } finally {
562
+ if (this.topoffPromise === p) this.topoffPromise = void 0;
563
+ }
564
+ }
565
+ /**
566
+ * Detached async top-off (opt-in via `prepay.async`). When below the watermark
567
+ * and no top-off is in flight, fire a deposit WITHOUT awaiting it in the op
568
+ * path. Documented races: the balance read is point-in-time so the trigger can
569
+ * be stale, and a deposit settling between an op's read and this check can make
570
+ * the top-off redundant; the single-flight flag bounds these to at most one
571
+ * outstanding deposit, but cannot serialize against an op already past its own
572
+ * check. Use the (default) synchronous single-shot for exactly-bounded spend.
573
+ *
574
+ * Account-key mode: there is no signing wallet, so the detached
575
+ * deposit is dispatched through `runAccountTopoff()` (the `topoffPayer` hook)
576
+ * instead of `runDeposit` (which throws `no_signer` in account mode). Wallet
577
+ * mode is completely unchanged below.
578
+ */
579
+ maybeAsyncTopoff() {
580
+ if (!this.prepay?.async || this.topoffInFlight || this.knownCredits === void 0 || this.knownCredits >= this.watermarkCredits()) {
581
+ return;
582
+ }
583
+ if (this.maxSessionSpendUsd !== void 0 && this.sessionSpentUsd + this.prepay.topoff > this.maxSessionSpendUsd) {
584
+ return;
585
+ }
586
+ this.topoffInFlight = true;
587
+ if (this.accountKey) {
588
+ void this.runAccountTopoff().catch(() => {
589
+ }).finally(() => {
590
+ this.topoffInFlight = false;
591
+ });
592
+ return;
593
+ }
594
+ void this.runDeposit(this.prepay.topoff, { bypassPerOpCap: true }).catch(() => {
595
+ }).finally(() => {
596
+ this.topoffInFlight = false;
597
+ });
598
+ }
599
+ /**
600
+ * Whether a top-off of `prepay.topoff` fits under the cumulative SESSION cap.
601
+ * Top-offs are deliberately NOT gated on the per-op `maxSpendUsd` (which bounds
602
+ * individual pay-per-op charges); when over the session cap we downgrade to
603
+ * pay-per-op rather than throwing.
604
+ */
605
+ topoffFitsSessionCap() {
606
+ if (this.maxSessionSpendUsd === void 0) return true;
607
+ return this.sessionSpentUsd + this.prepay.topoff <= this.maxSessionSpendUsd;
608
+ }
609
+ /**
610
+ * Single source of truth for BOTH the EIP-712-signed pathname (`path`) and the
611
+ * URL to fetch (`url`), so they can never diverge — a divergence silently
612
+ * breaks identity auth: the worker verifies over the RECEIVED path and
613
+ * recovers a phantom address if it differs from what the client signed.
614
+ * `base` is the un-prefixed pathname; the fetched/signed path is `/v1` + `base`
615
+ * (list-keys overrides it to `/v1/kv`). `query` is appended to `url`
616
+ * ONLY — EIP-712 binds the pathname, never the query string.
617
+ */
618
+ route(spec) {
619
+ const path = spec.versioned ?? `${V1}${spec.base}`;
620
+ const q = spec.query ? `?${spec.query}` : "";
621
+ return { path, url: `${this.endpoint}${path}${q}` };
622
+ }
623
+ // kv entry route (set/get/delete). `digest` is base64url (from hashKey(), which
624
+ // returns toBase64Url output — URL-safe [A-Za-z0-9_-], not hex), so no extra
625
+ // encoding is needed and the signed path matches the fetched path byte-for-byte.
626
+ kvRoute(digest) {
627
+ return this.route({ base: `/kv/${digest}` });
628
+ }
629
+ /**
630
+ * Per-op auth headers. In account-key mode this is the `Authorization: Bearer
631
+ * ak_…` header (server hashes it to name storage + debit credits); in wallet
632
+ * mode it is the EIP-712 identity signature. Used by every op so the same call
633
+ * site picks the right scheme. Async to share the signature of `identityHeaders`.
634
+ */
635
+ async authHeaders(method, path) {
636
+ if (this.accountKey) return (0, import_core.buildBearerHeaders)(this.accountKey);
637
+ return { ...await this.identityHeaders(method, path) };
638
+ }
639
+ /**
640
+ * The signing wallet, asserted present. Every x402 path (set/get top-off + 402
641
+ * fallback, deposit) is gated behind `!this.accountKey` and so always has a
642
+ * signer; this narrows the optional type at those call sites (and fails loudly
643
+ * if that invariant is ever broken).
644
+ */
645
+ requireSigner() {
646
+ if (!this.signer) {
647
+ throw new import_core2.AgentKVError(
648
+ "no signer: this operation requires a signing wallet",
649
+ "invalid_config",
650
+ 0
651
+ );
652
+ }
653
+ return this.signer;
654
+ }
655
+ /** EIP-712 identity headers with the deployment host bound into the signature (prevents cross-deployment signature replay). */
656
+ identityHeaders(method, path) {
657
+ if (!this.signer) {
658
+ throw new import_core2.AgentKVError(
659
+ "no signer: this operation requires a signing wallet",
660
+ "invalid_config",
661
+ 0
662
+ );
663
+ }
664
+ return (0, import_core.buildIdentityHeaders)(this.signer, {
665
+ method,
666
+ path,
667
+ host: new URL(this.endpoint).host,
668
+ network: this.network
669
+ });
670
+ }
671
+ /**
672
+ * Shared money/transport orchestrator behind set() and getInternal() — the single copy of
673
+ * the flow both share: account-key (bearer) mode with proactive + hard-402 top-off and the
674
+ * inline-payer path; wallet mode with the proactive single-shot, credit path, and 402
675
+ * pay-and-retry — including the single-flight top-off accounting and settled-txHash spend
676
+ * gating. Per-op differences (method/body, credit cost, 404 handling, success/inline
677
+ * parsing) come from `spec`. The CALLER must claim the single-flight top-off SYNCHRONOUSLY
678
+ * (before any await) and pass it in `flight`; it may be re-claimed here on a cold-start hard
679
+ * 402, and the caller's `finally` releases it.
680
+ */
681
+ async performOp(flight, spec) {
682
+ const { path, url, idempotencyKey, creditCostUsd, label } = spec;
683
+ if (this.accountKey) {
684
+ let toppedOff = false;
685
+ if (flight.claimed && this.topoffPayer && this.topoffFitsSessionCap()) {
686
+ try {
687
+ await this.runSharedTopoff();
688
+ toppedOff = true;
689
+ } catch {
690
+ }
691
+ }
692
+ this.maybeAsyncTopoff();
693
+ this.assertSpend(creditCostUsd);
694
+ const sendBearer = () => this.fetchWithRetry(
695
+ url,
696
+ () => spec.buildRequest({
697
+ "Idempotency-Key": idempotencyKey,
698
+ ...(0, import_core.buildBearerHeaders)(this.accountKey)
699
+ })
700
+ );
701
+ let res2 = await sendBearer();
702
+ this.trackBalance(res2);
703
+ if (res2.status === 402 && this.topoffPayer && !toppedOff) {
704
+ if (!flight.claimed) flight.claimed = this.tryClaimTopoffOnFault();
705
+ if (flight.claimed && this.topoffFitsSessionCap()) {
706
+ await this.runSharedTopoff();
707
+ res2 = await sendBearer();
708
+ this.trackBalance(res2);
709
+ } else if (this.topoffPromise) {
710
+ await this.topoffPromise.catch(() => {
711
+ });
712
+ res2 = await sendBearer();
713
+ this.trackBalance(res2);
714
+ }
715
+ }
716
+ if (res2.status === 402 && this.opInlinePayer && !this.topoffPayer) {
717
+ const inlineCeilingUsd = this.inlineOpCeilingUsd();
718
+ this.assertSpend(inlineCeilingUsd);
719
+ const reqInit = spec.buildRequest({
720
+ "Idempotency-Key": idempotencyKey,
721
+ ...(0, import_core.buildBearerHeaders)(this.accountKey)
722
+ });
723
+ const inlineRes = await this.opInlinePayer({
724
+ url,
725
+ method: spec.method,
726
+ body: reqInit.body,
727
+ headers: reqInit.headers,
728
+ // The hook MUST NOT settle more than the effective per-op ceiling.
729
+ maxAmountAtomic: Math.round(inlineCeilingUsd * 1e6)
730
+ });
731
+ if (inlineRes.status === 404 && spec.notFound) return spec.notFound();
732
+ if (inlineRes.status < 200 || inlineRes.status >= 300) {
733
+ throw this.errorFromBody(inlineRes.status, inlineRes.body, label);
734
+ }
735
+ this.recordSpend(this.inlineSettledAmountUsd(inlineRes.headers) ?? creditCostUsd);
736
+ return spec.parseInline(inlineRes);
737
+ }
738
+ if (res2.status === 404 && spec.notFound) return spec.notFound();
739
+ if (!res2.ok) throw await this.asError(res2, label);
740
+ this.recordSpend(creditCostUsd);
741
+ return spec.parseSuccess(res2);
742
+ }
743
+ if (flight.claimed && this.challengeTemplate && this.topoffFitsSessionCap()) {
744
+ let paymentSignature;
745
+ try {
746
+ paymentSignature = await (0, import_core.buildPaymentHeader)(this.requireSigner(), this.challengeTemplate, {
747
+ amountAtomic: this.topoffAtomic,
748
+ expectedNetwork: this.network,
749
+ // Pin the nonce to the op's idempotency key so a retry reuses the auth and the
750
+ // server dedupes the mint + the op.
751
+ nonce: (0, import_core.nonceFromIdempotencyKey)(idempotencyKey)
752
+ });
753
+ } catch {
754
+ this.challengeTemplate = void 0;
755
+ }
756
+ if (paymentSignature !== void 0) {
757
+ const res2 = await this.fetchWithRetry(
758
+ url,
759
+ () => spec.buildRequest({
760
+ "Idempotency-Key": idempotencyKey,
761
+ "PAYMENT-SIGNATURE": paymentSignature
762
+ })
763
+ );
764
+ this.trackBalance(res2);
765
+ if (res2.status === 404 && spec.notFound) return spec.notFound();
766
+ if (res2.status !== 402) {
767
+ if (!res2.ok) throw await this.asError(res2, label);
768
+ if (this.settledTxHash(res2)) this.recordSpend(this.prepay.topoff);
769
+ return spec.parseSuccess(res2);
770
+ }
771
+ }
772
+ }
773
+ this.maybeAsyncTopoff();
774
+ let res = await this.fetchWithRetry(
775
+ url,
776
+ async () => spec.buildRequest({
777
+ "Idempotency-Key": idempotencyKey,
778
+ ...await this.identityHeaders(spec.method, path)
779
+ })
780
+ );
781
+ this.trackBalance(res);
782
+ if (res.status === 402) {
783
+ const challenge = res.headers.get("PAYMENT-REQUIRED");
784
+ if (!challenge) {
785
+ throw await this.asError(res, "payment required but no PAYMENT-REQUIRED challenge");
786
+ }
787
+ if (!flight.claimed) flight.claimed = this.tryClaimTopoffOnFault();
788
+ const topoffHere = flight.claimed && this.topoffFitsSessionCap();
789
+ const usd = topoffHere ? this.prepay.topoff : (0, import_core.challengePriceUsd)(challenge, void 0, this.network);
790
+ if (!topoffHere) {
791
+ this.assertOpPriceCeiling(usd);
792
+ this.assertSpend(usd);
793
+ }
794
+ const paymentSignature = await (0, import_core.buildPaymentHeader)(this.requireSigner(), challenge, {
795
+ amountAtomic: topoffHere ? this.topoffAtomic : void 0,
796
+ expectedNetwork: this.network,
797
+ nonce: (0, import_core.nonceFromIdempotencyKey)(idempotencyKey)
798
+ });
799
+ res = await this.fetchWithRetry(
800
+ url,
801
+ () => spec.buildRequest({
802
+ "Idempotency-Key": idempotencyKey,
803
+ "PAYMENT-SIGNATURE": paymentSignature
804
+ })
805
+ );
806
+ this.trackBalance(res);
807
+ if (res.ok && (!topoffHere || this.settledTxHash(res))) this.recordSpend(usd);
808
+ }
809
+ if (res.status === 404 && spec.notFound) return spec.notFound();
810
+ if (!res.ok) throw await this.asError(res, label);
811
+ return spec.parseSuccess(res);
812
+ }
813
+ /**
814
+ * Write an encrypted value. The value is JSON-stringified and AES-256-GCM
815
+ * encrypted client-side; the server only ever stores ciphertext. `null` and
816
+ * `undefined` are rejected (`invalid_value`) so a `null` from get() unambiguously
817
+ * means "missing key" — use delete() to remove a key. Tries the credit path first
818
+ * (EIP-712 identity signature); if credits are insufficient the server returns a 402
819
+ * x402 challenge and the client pays. A stable Idempotency-Key is reused across that
820
+ * retry so the write is exactly-once.
821
+ */
822
+ async set(key, value, opts = {}) {
823
+ const flight = { claimed: this.tryClaimTopoff() };
824
+ try {
825
+ const plaintext = JSON.stringify(value);
826
+ if (value === null || plaintext === void 0) {
827
+ throw new import_core2.AgentKVError(
828
+ "cannot store null or undefined; use delete() to remove a key",
829
+ "invalid_value",
830
+ 0
831
+ );
832
+ }
833
+ const km = await this.getKeyMaterial();
834
+ const digest = hashKey(km.mac, key);
835
+ const ciphertext = await encrypt(km.value, plaintext, digest);
836
+ const body = {
837
+ value: ciphertext,
838
+ key_name: await encrypt(km.keyName, key)
839
+ };
840
+ if (opts.ttlDays !== void 0) body.ttl_days = opts.ttlDays;
841
+ if (opts.strictTtl !== void 0) body.strict_ttl = opts.strictTtl;
842
+ const payload = JSON.stringify(body);
843
+ const idempotencyKey = opts.idempotencyKey ?? (0, import_core.freshNonce)();
844
+ const { path, url } = this.kvRoute(digest);
845
+ return await this.performOp(flight, {
846
+ method: "POST",
847
+ path,
848
+ url,
849
+ idempotencyKey,
850
+ creditCostUsd: ACCOUNT_WRITE_USD,
851
+ label: "set failed",
852
+ buildRequest: (headers) => ({
853
+ method: "POST",
854
+ headers: { "content-type": "application/json", ...headers },
855
+ body: payload
856
+ }),
857
+ parseSuccess: async (res) => await res.json(),
858
+ parseInline: async (inlineRes) => JSON.parse(inlineRes.body)
859
+ });
860
+ } finally {
861
+ if (flight.claimed) this.topoffInFlight = false;
862
+ }
863
+ }
864
+ /**
865
+ * Read and decrypt a value. Tries the credit path first (EIP-712 identity);
866
+ * if credits are insufficient the server returns a 402 x402 challenge and the
867
+ * client pays, then retries. Returns null if the key is missing or expired (404);
868
+ * stored values are never null (set rejects it), so null unambiguously means absent.
869
+ */
870
+ async get(key, opts = {}) {
871
+ const { value } = await this.getInternal(key, opts);
872
+ return value;
873
+ }
874
+ /**
875
+ * Like `get`, but ALSO surfaces the machine-readable usage envelope the server
876
+ * attaches to a paid read's success body — a separate, additive
877
+ * accessor so `get()` keeps its narrower `T | null` signature (no existing
878
+ * caller breaks). `usage` is absent when the key was missing/expired (404 —
879
+ * the read op itself is never charged on a miss, see the DO's 404-before-charge
880
+ * precheck — though a proactive top-off, if one was triggered for this call,
881
+ * is a separate deposit charge and can still happen alongside a miss) or when
882
+ * talking to a server that predates the usage envelope.
883
+ */
884
+ async getWithUsage(key, opts = {}) {
885
+ return this.getInternal(key, opts);
886
+ }
887
+ /**
888
+ * Shared implementation behind `get`/`getWithUsage`. Tries the credit path
889
+ * first (EIP-712 identity); if credits are insufficient the server returns a
890
+ * 402 x402 challenge and the client pays, then retries. `value` is null if
891
+ * the key is missing or expired (404); stored values are never null (set
892
+ * rejects it), so null unambiguously means absent.
893
+ */
894
+ async getInternal(key, opts = {}) {
895
+ const flight = { claimed: this.tryClaimTopoff() };
896
+ try {
897
+ const digest = hashKey((await this.getKeyMaterial()).mac, key);
898
+ const { path, url } = this.kvRoute(digest);
899
+ const idempotencyKey = opts.idempotencyKey ?? (0, import_core.freshNonce)();
900
+ const parseBody = async (raw) => {
901
+ const data = JSON.parse(raw);
902
+ const decryptedText = await this.decryptValue(data.value, key);
903
+ return { value: JSON.parse(decryptedText), usage: data.usage };
904
+ };
905
+ return await this.performOp(flight, {
906
+ method: "GET",
907
+ path,
908
+ url,
909
+ idempotencyKey,
910
+ creditCostUsd: ACCOUNT_READ_USD,
911
+ label: "get failed",
912
+ buildRequest: (headers) => ({ method: "GET", headers }),
913
+ parseSuccess: async (res) => parseBody(await res.text()),
914
+ parseInline: async (inlineRes) => parseBody(inlineRes.body),
915
+ notFound: () => ({ value: null })
916
+ });
917
+ } finally {
918
+ if (flight.claimed) this.topoffInFlight = false;
919
+ }
920
+ }
921
+ /**
922
+ * Delete a key. Free operation. Authenticated with the account-key bearer in
923
+ * account mode, else an EIP-712 identity signature (fresh nonce + timestamp).
924
+ * The digest is computed from the local key material either way.
925
+ */
926
+ async delete(key) {
927
+ const digest = hashKey((await this.getKeyMaterial()).mac, key);
928
+ const { path, url } = this.kvRoute(digest);
929
+ const res = await this.fetchWithRetry(url, async () => ({
930
+ method: "DELETE",
931
+ headers: { ...await this.authHeaders("DELETE", path) }
932
+ }));
933
+ if (!res.ok) {
934
+ throw await this.asError(res, "delete failed");
935
+ }
936
+ return await res.json();
937
+ }
938
+ /**
939
+ * List the wallet's keys. The server returns opaque per-wallet digests plus each key's
940
+ * ENCRYPTED name; this decrypts the names locally and returns them — the server never
941
+ * sees a plaintext key name. Free (EIP-712 identity signed). Paginated: pass the returned
942
+ * `cursor` to fetch the next page; `cursor` is null once exhausted.
943
+ */
944
+ async listKeys(opts = {}) {
945
+ const km = await this.getKeyMaterial();
946
+ const params = new URLSearchParams();
947
+ if (opts.cursor) params.set("cursor", opts.cursor);
948
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
949
+ const qs = params.toString();
950
+ const { path, url } = this.route({
951
+ base: "/list-keys",
952
+ versioned: `${V1}/kv`,
953
+ query: qs || void 0
954
+ });
955
+ const res = await this.fetchWithRetry(url, async () => ({
956
+ method: "GET",
957
+ headers: { ...await this.authHeaders("GET", path) }
958
+ }));
959
+ if (!res.ok) throw await this.asError(res, "list-keys failed");
960
+ const data = await res.json();
961
+ const keys = (await Promise.all(
962
+ data.items.filter((i) => i.key_name != null).map(async (i) => {
963
+ try {
964
+ return await decrypt(km.keyName, i.key_name);
965
+ } catch {
966
+ return null;
967
+ }
968
+ })
969
+ )).filter((k) => k !== null);
970
+ return { keys, cursor: data.cursor };
971
+ }
972
+ /**
973
+ * Read the pre-paid credit balance. Free. Account-key bearer in account mode,
974
+ * else an EIP-712 identity signature.
975
+ */
976
+ async balance() {
977
+ const { path, url } = this.route({ base: "/credits/balance" });
978
+ const res = await this.fetchWithRetry(url, async () => ({
979
+ method: "GET",
980
+ headers: { ...await this.authHeaders("GET", path) }
981
+ }));
982
+ this.trackBalance(res);
983
+ if (!res.ok) {
984
+ throw await this.asError(res, "balance failed");
985
+ }
986
+ const body = await res.json();
987
+ if (this.prepay && Number.isFinite(body.balance)) this.knownCredits = body.balance;
988
+ return body.balance;
989
+ }
990
+ /**
991
+ * Buy credits with an x402 payment. `amountUsd` must be at least $1; any
992
+ * amount is accepted (no fixed tiers). This settles on-chain once via the
993
+ * facilitator; the returned credits are then spendable by set/get with no
994
+ * further payment.
995
+ */
996
+ async deposit(amountUsd, opts = {}) {
997
+ if (this.accountKey && this.topoffPayer) {
998
+ const runAccountDeposit = async () => {
999
+ this.assertSpend(amountUsd);
1000
+ await this.runAccountTopoff(amountUsd);
1001
+ const balance = await this.balance();
1002
+ return { credits_added: Math.round(amountUsd / CREDIT_VALUE_USD), balance };
1003
+ };
1004
+ if (this.topoffInFlight) {
1005
+ return runAccountDeposit();
1006
+ }
1007
+ this.topoffInFlight = true;
1008
+ try {
1009
+ return await runAccountDeposit();
1010
+ } finally {
1011
+ this.topoffInFlight = false;
1012
+ }
1013
+ }
1014
+ if (!this.prepay || this.topoffInFlight) {
1015
+ return this.runDeposit(amountUsd, opts);
1016
+ }
1017
+ this.topoffInFlight = true;
1018
+ try {
1019
+ return await this.runDeposit(amountUsd, opts);
1020
+ } finally {
1021
+ this.topoffInFlight = false;
1022
+ }
1023
+ }
1024
+ /**
1025
+ * Account-key top-off: delegate payment of `${endpoint}/account/deposit` to the
1026
+ * configured `topoffPayer` (a managed account has no signing wallet to sign an
1027
+ * x402 payment). The caller must hold the single-flight claim and have checked
1028
+ * `topoffFitsSessionCap()`. On resolve (= the deposit SETTLED) the top-off is
1029
+ * recorded against the session budget only — top-offs are credit purchases, not
1030
+ * per-op charges, so the per-op cap is deliberately not consulted (mirrors the
1031
+ * wallet-mode top-off budget rules). A rejection is wrapped as
1032
+ * `account_topoff_failed`; the ak_ bearer is never included in the message.
1033
+ */
1034
+ /**
1035
+ * `amountUsd` generalizes this beyond the fixed `prepay.topoff` amount
1036
+ * so `deposit()` can reuse it for a caller-chosen amount. OMITTED (the no-arg
1037
+ * call from the proactive/hard-402/async paths above), it defaults to
1038
+ * `prepay.topoff` and its precomputed `topoffAtomic` ceiling — BYTE-FOR-BYTE the
1039
+ * stage-1 behavior. An EXPLICIT `amountUsd` (from `deposit()`, which may be
1040
+ * called with no `prepay` configured at all) is validated here the same way
1041
+ * `runDeposit` validates its wallet-mode amount, since it never passed through
1042
+ * the constructor's `prepay.topoff` guard.
1043
+ */
1044
+ async runAccountTopoff(amountUsd) {
1045
+ let amount;
1046
+ let maxAmountAtomic;
1047
+ if (amountUsd === void 0) {
1048
+ amount = this.prepay.topoff;
1049
+ maxAmountAtomic = this.topoffAtomic;
1050
+ } else {
1051
+ const atomic = toWholeAtomicUsd(amountUsd);
1052
+ if (atomic === null || !(amountUsd >= 1)) {
1053
+ throw new import_core2.AgentKVError(
1054
+ "amountUsd must be >= $1 and a whole number of atomic USDC units",
1055
+ "invalid_config",
1056
+ 0
1057
+ );
1058
+ }
1059
+ amount = amountUsd;
1060
+ maxAmountAtomic = atomic;
1061
+ }
1062
+ try {
1063
+ await this.topoffPayer({
1064
+ depositUrl: this.route({ base: "/account/deposit" }).url,
1065
+ accountKey: this.accountKey,
1066
+ amountUsd: amount,
1067
+ maxAmountAtomic
1068
+ });
1069
+ } catch (e) {
1070
+ const detail = e instanceof Error ? e.message : String(e);
1071
+ throw new import_core2.AgentKVError(
1072
+ `account top-off failed: ${detail} \u2014 check the payer wallet's USDC balance (e.g. 'awal balance'; fund by sending USDC to its address)`,
1073
+ "account_topoff_failed",
1074
+ 0
1075
+ );
1076
+ }
1077
+ this.recordSpend(amount);
1078
+ }
1079
+ async runDeposit(amountUsd, opts) {
1080
+ if (this.accountKey) {
1081
+ throw new import_core2.AgentKVError(
1082
+ `Account-key mode has no signing wallet. Fund this account with fundAccount(payerKeyOrSigner, amountUsd), or via 'agentkv fund', or awal: awal x402 pay ${this.route({ base: "/account/deposit" }).url} --headers '{"Authorization":"Bearer <ak>"}'.`,
1083
+ "no_signer",
1084
+ 0
1085
+ );
1086
+ }
1087
+ const amountAtomic = toWholeAtomicUsd(amountUsd);
1088
+ if (amountAtomic === null || !(amountUsd >= 1)) {
1089
+ throw new import_core2.AgentKVError(
1090
+ "deposit amountUsd must be >= $1 and a whole number of atomic USDC units",
1091
+ "invalid_config",
1092
+ 0
1093
+ );
1094
+ }
1095
+ this.assertSpend(amountUsd, opts);
1096
+ const opKey = opts.idempotencyKey ?? (0, import_core.freshNonce)();
1097
+ const { url } = this.route({ base: "/credits/deposit" });
1098
+ let res = await this.fetchWithRetry(url, () => ({
1099
+ method: "POST",
1100
+ headers: { "Idempotency-Key": opKey }
1101
+ }));
1102
+ this.trackBalance(res);
1103
+ if (res.status === 402) {
1104
+ const challenge = res.headers.get("PAYMENT-REQUIRED");
1105
+ if (!challenge) {
1106
+ throw await this.asError(res, "payment required but no PAYMENT-REQUIRED challenge");
1107
+ }
1108
+ const paymentSignature = await (0, import_core.buildPaymentHeader)(this.requireSigner(), challenge, {
1109
+ amountAtomic,
1110
+ expectedNetwork: this.network,
1111
+ expectedPayTo: opts.expectedPayTo,
1112
+ nonce: (0, import_core.nonceFromIdempotencyKey)(opKey)
1113
+ });
1114
+ res = await this.fetchWithRetry(url, () => ({
1115
+ method: "POST",
1116
+ headers: { "Idempotency-Key": opKey, "PAYMENT-SIGNATURE": paymentSignature }
1117
+ }));
1118
+ this.trackBalance(res);
1119
+ }
1120
+ if (!res.ok) {
1121
+ throw await this.asError(res, "deposit failed");
1122
+ }
1123
+ this.recordSpend(amountUsd);
1124
+ const result = await res.json();
1125
+ if (this.prepay && Number.isFinite(result.balance)) this.knownCredits = result.balance;
1126
+ return result;
1127
+ }
1128
+ /**
1129
+ * Fund an ACCOUNT-KEY namespace — "payer funds, bearer owns". A CALLER-supplied
1130
+ * `signer` pays via x402 to add prepaid credits to THIS client's account (the
1131
+ * one named by its `ak_…` bearer). The payer and the owner are deliberately
1132
+ * DECOUPLED: the payer wallet signs the on-chain EIP-3009 authorization, while
1133
+ * the bearer — not the payer's address — owns the credited namespace. This is the
1134
+ * SDK counterpart of the server's `/account/deposit` route.
1135
+ *
1136
+ * Account-key mode ONLY. In WALLET mode the paying wallet IS the namespace, so
1137
+ * use `deposit()` instead — calling this throws `wrong_mode` before any network.
1138
+ *
1139
+ * `signer` is the PAYER: a viem account (must expose `address` + `signTypedData`)
1140
+ * or a raw `0x` private key (built into a viem account internally, mirroring the
1141
+ * constructor). `amountUsd` must be a whole number of dollars >= $1. UNLIKE
1142
+ * `deposit()` (which IS gated by both spend caps and counts toward session spend),
1143
+ * this explicit funding call is NOT gated by `maxSpendUsd`/`maxSessionSpendUsd` and
1144
+ * does not count toward session spend — the payer is an EXTERNAL wallet, not this
1145
+ * client's tracked budget. The local encryption key is never touched (funding does not encrypt).
1146
+ */
1147
+ async fundAccount(signer, amountUsd, opts = {}) {
1148
+ if (!this.accountKey) {
1149
+ throw new import_core2.AgentKVError(
1150
+ "fundAccount funds an account-key namespace; in wallet mode use deposit()",
1151
+ "wrong_mode",
1152
+ 0
1153
+ );
1154
+ }
1155
+ const payer = typeof signer === "string" ? (0, import_accounts.privateKeyToAccount)(signer) : signer;
1156
+ if (!payer?.address || typeof payer.signTypedData !== "function") {
1157
+ throw new import_core2.AgentKVError(
1158
+ "fundAccount: signer must be a 0x private key or a viem account (with address + signTypedData)",
1159
+ "invalid_config",
1160
+ 0
1161
+ );
1162
+ }
1163
+ if (!Number.isInteger(amountUsd) || amountUsd < 1) {
1164
+ throw new import_core2.AgentKVError(
1165
+ "fundAccount amountUsd must be a whole number of US dollars >= $1",
1166
+ "invalid_config",
1167
+ 0
1168
+ );
1169
+ }
1170
+ const amountAtomic = amountUsd * 1e6;
1171
+ const { url } = this.route({ base: "/account/deposit" });
1172
+ const bearer = (0, import_core.buildBearerHeaders)(this.accountKey);
1173
+ const idempotencyKey = opts.idempotencyKey ?? (0, import_core.freshNonce)();
1174
+ const nonce = (0, import_core.nonceFromIdempotencyKey)(idempotencyKey);
1175
+ let res = await this.fetchWithRetry(url, () => ({
1176
+ method: "POST",
1177
+ headers: { ...bearer, "Idempotency-Key": idempotencyKey }
1178
+ }));
1179
+ this.trackBalance(res);
1180
+ if (res.status === 402) {
1181
+ const challenge = res.headers.get("PAYMENT-REQUIRED");
1182
+ if (!challenge) {
1183
+ throw await this.asError(res, "payment required but no PAYMENT-REQUIRED challenge");
1184
+ }
1185
+ const paymentSignature = await (0, import_core.buildPaymentHeader)(payer, challenge, {
1186
+ amountAtomic,
1187
+ expectedNetwork: this.network,
1188
+ expectedPayTo: opts.expectedPayTo,
1189
+ nonce
1190
+ });
1191
+ res = await this.fetchWithRetry(url, () => ({
1192
+ method: "POST",
1193
+ headers: {
1194
+ ...bearer,
1195
+ "Idempotency-Key": idempotencyKey,
1196
+ "PAYMENT-SIGNATURE": paymentSignature
1197
+ }
1198
+ }));
1199
+ this.trackBalance(res);
1200
+ }
1201
+ if (!res.ok) {
1202
+ throw await this.asError(res, "fundAccount failed");
1203
+ }
1204
+ const result = await res.json();
1205
+ if (this.prepay && Number.isFinite(result.balance)) this.knownCredits = result.balance;
1206
+ return result;
1207
+ }
1208
+ /** Shared by `asError` (a real `Response`) and the `opInlinePayer` path (a plain `{status,body}`). */
1209
+ errorFromBody(status, bodyText, fallback) {
1210
+ let detail = fallback, code = "request_failed";
1211
+ try {
1212
+ const body = JSON.parse(bodyText);
1213
+ if (body?.error) detail = body.error;
1214
+ if (body?.code) code = body.code;
1215
+ } catch {
1216
+ }
1217
+ return new import_core2.AgentKVError(`AgentKV ${status}: ${detail}`, code, status);
1218
+ }
1219
+ async asError(res, fallback) {
1220
+ return this.errorFromBody(res.status, await res.text(), fallback);
1221
+ }
1222
+ /**
1223
+ * The settled amount (USD) from an `opInlinePayer` response's PAYMENT-RESPONSE
1224
+ * header — the inline-path mirror of `settledTxHash()` above, but reading a
1225
+ * plain `Record<string,string>` (the hook's own headers, not a `Response`) and
1226
+ * returning the `amount` field instead of the `txHash`. Case-insensitive header
1227
+ * lookup: an external transport (e.g. awal) is not guaranteed to preserve the
1228
+ * worker's exact `PAYMENT-RESPONSE` casing. Returns `undefined` when the header
1229
+ * is absent/unparsable OR the op settled nothing (served from existing credits,
1230
+ * `txHash: ""`) — callers fall back to the credit-equivalent op price.
1231
+ */
1232
+ inlineSettledAmountUsd(headers) {
1233
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === "payment-response");
1234
+ const header = key ? headers[key] : void 0;
1235
+ if (!header) return void 0;
1236
+ try {
1237
+ const parsed = JSON.parse((0, import_core.decodeBase64Utf8)(header));
1238
+ if (typeof parsed.txHash !== "string" || parsed.txHash === "") return void 0;
1239
+ const atomic = Number(parsed.amount);
1240
+ return Number.isFinite(atomic) && atomic > 0 ? atomic / 1e6 : void 0;
1241
+ } catch {
1242
+ return void 0;
1243
+ }
1244
+ }
1245
+ };
1246
+ // Annotate the CommonJS export names for ESM import in node:
1247
+ 0 && (module.exports = {
1248
+ ACCOUNT_READ_USD,
1249
+ ACCOUNT_WRITE_USD,
1250
+ AgentKV,
1251
+ AgentKVError,
1252
+ CREDIT_VALUE_USD,
1253
+ DEFAULT_MAX_OP_USD,
1254
+ SpendCapError,
1255
+ VERSION,
1256
+ decrypt,
1257
+ deriveKeyMaterial,
1258
+ encrypt,
1259
+ generateAccountKey,
1260
+ hashKey,
1261
+ isAccountKeyFormat
1262
+ });
1263
+ //# sourceMappingURL=index.cjs.map