@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/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/index.cjs +1263 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +584 -0
- package/dist/index.d.ts +584 -0
- package/dist/index.js +1240 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { Signer, UsageBlock } from '@agentx402-ai/core';
|
|
2
|
+
export { AgentKVError, Signer, SpendCapError, UsageBlock } from '@agentx402-ai/core';
|
|
3
|
+
|
|
4
|
+
/** Request passed to `topoffPayer` when the client needs an account-key top-off. */
|
|
5
|
+
interface TopoffPayerRequest {
|
|
6
|
+
/** `${endpoint}/account/deposit` — the ONLY URL a hook should ever pay. */
|
|
7
|
+
depositUrl: string;
|
|
8
|
+
/** The `ak_…` bearer that must authorize the deposit (`Authorization: Bearer <ak>`). */
|
|
9
|
+
accountKey: string;
|
|
10
|
+
/** Requested top-off in USD (= `prepay.topoff`, >= $1 — the server minimum). */
|
|
11
|
+
amountUsd: number;
|
|
12
|
+
/** Hard payment ceiling in atomic USDC units (1e6 = $1) the hook MUST enforce (e.g. awal `--max-amount`). */
|
|
13
|
+
maxAmountAtomic: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Request handed to `opInlinePayer`: the WHOLE encrypted account-key `set`/`get`
|
|
17
|
+
* op, ready to send as-is. The hook drives its OWN 402→pay→retry (e.g. via
|
|
18
|
+
* `awal x402 pay`) and returns the final response — the client never sees the
|
|
19
|
+
* intermediate 402 on this path.
|
|
20
|
+
*/
|
|
21
|
+
interface OpInlineRequest {
|
|
22
|
+
/** `${endpoint}/kv/<digest>` — the ONLY URL a hook should ever request. */
|
|
23
|
+
url: string;
|
|
24
|
+
method: "POST" | "GET";
|
|
25
|
+
/** Ciphertext JSON body (POST only; omitted on GET). */
|
|
26
|
+
body?: string;
|
|
27
|
+
/** Full request headers: bearer `Authorization`, `Idempotency-Key`, `content-type` (POST). */
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
/** Hard payment ceiling in atomic USDC units (1e6 = $1) the hook MUST enforce (e.g. awal `--max-amount`). */
|
|
30
|
+
maxAmountAtomic: number;
|
|
31
|
+
}
|
|
32
|
+
/** Final response from `opInlinePayer`, after its own 402→pay→retry has settled. */
|
|
33
|
+
interface OpInlineResponse {
|
|
34
|
+
status: number;
|
|
35
|
+
body: string;
|
|
36
|
+
headers: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
interface AgentKVCommon {
|
|
39
|
+
endpoint: string;
|
|
40
|
+
network?: string;
|
|
41
|
+
/** Per-paying-call USD cap; throws SpendCapError if exceeded. */
|
|
42
|
+
maxSpendUsd?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Cumulative USD cap across this client instance. **Best-effort**: the running
|
|
45
|
+
* total is a plain in-memory counter, so concurrent paying calls can race
|
|
46
|
+
* (both pass `assertSpend` before either `recordSpend`s) and modestly overshoot
|
|
47
|
+
* the cap. It is a guardrail, not a hard ledger — serialize paying calls if you
|
|
48
|
+
* need a strict bound.
|
|
49
|
+
*/
|
|
50
|
+
maxSessionSpendUsd?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Bounded internal retries on TRANSIENT failures (a thrown fetch / lost
|
|
53
|
+
* response, or a 5xx). Default 2 (3 attempts total). Retries reuse the op's
|
|
54
|
+
* stable Idempotency-Key (and pinned EIP-3009 nonce on paid ops) so a
|
|
55
|
+
* processed-but-unacked request dedupes server-side instead of double-charging.
|
|
56
|
+
* Kept small so a re-sent paid authorization stays within validBefore. Set 0 to disable.
|
|
57
|
+
*/
|
|
58
|
+
retries?: number;
|
|
59
|
+
/** Per-attempt request timeout in ms (aborts a hung-open connection so an op can't wedge forever). Default 30000. Pass 0 to disable. */
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
/** Injectable `fetch` for proxies / instrumentation / testing. Defaults to the global `fetch`. */
|
|
62
|
+
fetch?: typeof fetch;
|
|
63
|
+
/** Opt-in Discounted Prepay. When set, the client keeps a credit balance topped up. */
|
|
64
|
+
prepay?: {
|
|
65
|
+
/** Top off when the tracked balance (USD) falls below this. */
|
|
66
|
+
watermark: number;
|
|
67
|
+
/** Top-off amount in USD (>= 1). */
|
|
68
|
+
topoff: number;
|
|
69
|
+
/** Opt-in background top-off instead of synchronous single-shot. Default false. */
|
|
70
|
+
async?: boolean;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* ACCOUNT-KEY MODE ONLY: pays a prepaid-credit top-off to
|
|
74
|
+
* `${endpoint}/account/deposit` on this client's behalf (a managed account has
|
|
75
|
+
* no signing wallet). Called single-flight when the tracked balance falls below
|
|
76
|
+
* `prepay.watermark`, and as a fallback when an op hits an insufficient-credits
|
|
77
|
+
* 402. Resolve only once the deposit has SETTLED; a rejection surfaces as
|
|
78
|
+
* `account_topoff_failed` (fatal on the hard-402 path, swallowed on the
|
|
79
|
+
* proactive path). REQUIRED alongside `prepay` in account-key mode; rejected
|
|
80
|
+
* (`invalid_config`) in wallet mode or without `prepay` (it could never fire).
|
|
81
|
+
*/
|
|
82
|
+
topoffPayer?: (req: TopoffPayerRequest) => Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* ACCOUNT-KEY MODE ONLY: routes a WHOLE `set`/`get` op through an external inline
|
|
85
|
+
* x402 transport (e.g. `awal x402 pay`) instead of the stage-1 deposit top-off —
|
|
86
|
+
* opt-in, pay-per-op, no `prepay` required. Called on a hard 402 (insufficient
|
|
87
|
+
* credits): the client hands the hook the complete encrypted request
|
|
88
|
+
* (`{url, method, body, headers}`, already bearer-authenticated and
|
|
89
|
+
* Idempotency-Keyed) and the hook drives its OWN 402→pay→retry, returning the
|
|
90
|
+
* final `{status, body, headers}` for the client to parse/decrypt as usual.
|
|
91
|
+
*
|
|
92
|
+
* Mutually exclusive with `topoffPayer` PER OP: when BOTH are configured,
|
|
93
|
+
* `topoffPayer` (deposit top-off) always takes precedence and `opInlinePayer`
|
|
94
|
+
* is used only when `topoffPayer` is absent — see client/README.md. Rejected
|
|
95
|
+
* (`invalid_config`) in wallet mode (a signing wallet pays its own challenges).
|
|
96
|
+
*/
|
|
97
|
+
opInlinePayer?: (req: OpInlineRequest) => Promise<OpInlineResponse>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Construct with one of four auth shapes:
|
|
101
|
+
* - `{ privateKey }` — a raw wallet key (HKDF-derives the AES key from the KEY BYTES;
|
|
102
|
+
* wallet signs).
|
|
103
|
+
* - `{ signer, encryptionKey }` — a wallet signer + an explicit AES key.
|
|
104
|
+
* - `{ signer }` — a wallet signer; the AES key is SIGN-TO-DERIVED (HKDF over the wallet's
|
|
105
|
+
* signature of a fixed message).
|
|
106
|
+
* - `{ accountKey, encryptionKey }` — account-key mode: a managed account with
|
|
107
|
+
* NO signing wallet. Auth is an opaque `ak_…` bearer token (server hashes it to
|
|
108
|
+
* name storage + debits prepaid credits — no x402/EIP-712). There is no wallet
|
|
109
|
+
* to derive an AES key from, so an explicit `encryptionKey` is REQUIRED.
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ Encryption-key stability — the shapes are NOT interchangeable for the same wallet:
|
|
112
|
+
* - `{ privateKey: k }` and `{ signer: accountFrom(k) }` (sign-to-derive) derive DIFFERENT
|
|
113
|
+
* encryption keys (raw key bytes vs a signature over them). Switching a wallet between
|
|
114
|
+
* these two shapes changes the value/key-name/blind-index keys, so previously written
|
|
115
|
+
* data becomes unreadable AND unlisted (its lookup digests no longer match) with no error
|
|
116
|
+
* — reads simply return null. To move between them, or to use a KMS/hardware/MPC signer,
|
|
117
|
+
* pin an explicit `encryptionKey` so the AES key never depends on the signer shape.
|
|
118
|
+
* - Sign-to-derive requires a DETERMINISTIC signer whose signTypedData returns the standard
|
|
119
|
+
* 65-byte ECDSA form. Non-deterministic (some MPC/threshold) or alternate-encoding
|
|
120
|
+
* signers (EIP-2098 compact, ERC-1271/6492 smart accounts) either rotate the key per call
|
|
121
|
+
* or are rejected — those MUST pass an explicit `encryptionKey`.
|
|
122
|
+
* - The sign-to-derive signature IS secret-grade material: whoever obtains it can derive all
|
|
123
|
+
* of this wallet's AgentKV keys. Treat it like a private key (do not log / expose it).
|
|
124
|
+
*/
|
|
125
|
+
type AgentKVOptions = AgentKVCommon & ({
|
|
126
|
+
privateKey: `0x${string}`;
|
|
127
|
+
} | {
|
|
128
|
+
signer: Signer;
|
|
129
|
+
encryptionKey: Uint8Array | `0x${string}`;
|
|
130
|
+
} | {
|
|
131
|
+
signer: Signer;
|
|
132
|
+
} | {
|
|
133
|
+
accountKey: string;
|
|
134
|
+
encryptionKey: Uint8Array | `0x${string}`;
|
|
135
|
+
});
|
|
136
|
+
/** Per-write options. */
|
|
137
|
+
interface SetOptions {
|
|
138
|
+
/** Time-to-live in days (server default 90). */
|
|
139
|
+
ttlDays?: number;
|
|
140
|
+
/** If true, reads do not extend expiry (server default false). */
|
|
141
|
+
strictTtl?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Stable key identifying this logical write, reused across retries so a
|
|
144
|
+
* retried set() (after a crash/timeout) is exactly-once: the server hits its
|
|
145
|
+
* idempotency record instead of charging again. Defaults to a fresh value
|
|
146
|
+
* (each call is a distinct write).
|
|
147
|
+
*/
|
|
148
|
+
idempotencyKey?: string;
|
|
149
|
+
}
|
|
150
|
+
/** Per-read options. */
|
|
151
|
+
interface GetOptions {
|
|
152
|
+
/**
|
|
153
|
+
* Stable key making a retried get() exactly-once: the same key hits the
|
|
154
|
+
* server's read idempotency record (credit path) and pins the EIP-3009 nonce
|
|
155
|
+
* (paid path) instead of charging again. Defaults to a fresh value per call
|
|
156
|
+
* (each get() is a distinct, separately-charged read).
|
|
157
|
+
*/
|
|
158
|
+
idempotencyKey?: string;
|
|
159
|
+
}
|
|
160
|
+
/** Result of a successful write. */
|
|
161
|
+
interface SetResult {
|
|
162
|
+
ok: true;
|
|
163
|
+
expires_at: string;
|
|
164
|
+
/** Machine-readable usage envelope for this write. Absent only if the server predates it. */
|
|
165
|
+
usage?: UsageBlock;
|
|
166
|
+
}
|
|
167
|
+
/** Result of a successful delete. */
|
|
168
|
+
interface DeleteResult {
|
|
169
|
+
ok: true;
|
|
170
|
+
}
|
|
171
|
+
/** Result of a successful credit deposit. */
|
|
172
|
+
interface DepositResult {
|
|
173
|
+
credits_added: number;
|
|
174
|
+
balance: number;
|
|
175
|
+
}
|
|
176
|
+
/** Standard AgentKV error response body. */
|
|
177
|
+
interface ErrorBody {
|
|
178
|
+
error: string;
|
|
179
|
+
code: string;
|
|
180
|
+
hint?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Mint a fresh account key: `ak_` followed by the hex of AK_RANDOM_BYTES
|
|
185
|
+
* cryptographically-random bytes. This is the bearer secret — knowing it IS the
|
|
186
|
+
* capability to read/write the account. Persist it securely (e.g. a keystore);
|
|
187
|
+
* the worker stores only its hash, so a lost key is unrecoverable.
|
|
188
|
+
*/
|
|
189
|
+
declare function generateAccountKey(): string;
|
|
190
|
+
/** True iff `s` is a string in the canonical `ak_<64 lowercase hex>` shape. */
|
|
191
|
+
declare function isAccountKeyFormat(s: unknown): s is string;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* All keys derived from one per-wallet `ikm` with domain-separated HKDF labels — so the
|
|
195
|
+
* value key, the key-name key, and the blind-index MAC key are independent.
|
|
196
|
+
*/
|
|
197
|
+
interface KeyMaterial {
|
|
198
|
+
value: Uint8Array;
|
|
199
|
+
keyName: Uint8Array;
|
|
200
|
+
mac: Uint8Array;
|
|
201
|
+
}
|
|
202
|
+
declare function deriveKeyMaterial(ikm: Uint8Array): KeyMaterial;
|
|
203
|
+
/**
|
|
204
|
+
* Blind-index digest for a key NAME: `scheme_tag ‖ HMAC-SHA256(macKey, NFC(name))`,
|
|
205
|
+
* url-safe-base64. Deterministic and per-wallet (macKey is per-wallet), so the server
|
|
206
|
+
* looks up by an opaque token it cannot invert or correlate across wallets. NFC is the
|
|
207
|
+
* frozen normalization (case-sensitive, whitespace-preserving). Used by set/get/delete/list
|
|
208
|
+
* to look a value up under an opaque per-wallet token the server cannot invert.
|
|
209
|
+
*/
|
|
210
|
+
declare function hashKey(macKey: Uint8Array, name: string): string;
|
|
211
|
+
/**
|
|
212
|
+
* Encrypt plaintext with AES-256-GCM, emitting the versioned self-describing envelope
|
|
213
|
+
* `base64( magic ‖ ver ‖ suite ‖ kdf_id ‖ IV ‖ ct+tag )`. The header (and, when `aad` is
|
|
214
|
+
* supplied, the key's digest) is bound into the AAD.
|
|
215
|
+
*/
|
|
216
|
+
declare function encrypt(key: Uint8Array, plaintext: string, aad?: string): Promise<string>;
|
|
217
|
+
/**
|
|
218
|
+
* Decrypt a value produced by encrypt(). Requires the versioned envelope (magic + known
|
|
219
|
+
* ver/suite/kdf) and verifies the AAD-bound header (and, when `aad` is supplied, the key's
|
|
220
|
+
* digest — so a value the server serves for the WRONG key fails the tag instead of decrypting).
|
|
221
|
+
*/
|
|
222
|
+
declare function decrypt(key: Uint8Array, packed: string, aad?: string): Promise<string>;
|
|
223
|
+
|
|
224
|
+
declare const VERSION = "0.1.0";
|
|
225
|
+
|
|
226
|
+
/** USD value of one prepaid credit ($1 mints 10,000 credits). */
|
|
227
|
+
declare const CREDIT_VALUE_USD = 0.0001;
|
|
228
|
+
declare const ACCOUNT_READ_USD = 0.0003;
|
|
229
|
+
declare const ACCOUNT_WRITE_USD = 0.0005;
|
|
230
|
+
/**
|
|
231
|
+
* Built-in ceiling on a SERVER-QUOTED per-op price when no `maxSpendUsd` is configured.
|
|
232
|
+
* The advertised op price is ~$0.005; without this, a compromised or spoofed worker could
|
|
233
|
+
* answer a routine read with a 402 challenge for the wallet's entire balance and the client
|
|
234
|
+
* would sign the EIP-3009 authorization (the default config has no per-op cap). Callers who
|
|
235
|
+
* legitimately need a pricier op opt in explicitly via `maxSpendUsd`.
|
|
236
|
+
*/
|
|
237
|
+
declare const DEFAULT_MAX_OP_USD = 0.05;
|
|
238
|
+
declare class AgentKV {
|
|
239
|
+
/**
|
|
240
|
+
* The signing wallet. `undefined` in account-key mode (a managed account has no
|
|
241
|
+
* wallet that can sign) — the `ak_…` bearer token is the identity instead.
|
|
242
|
+
*/
|
|
243
|
+
readonly signer?: Signer;
|
|
244
|
+
/**
|
|
245
|
+
* The wallet address, the per-wallet namespace. In account-key mode there is no
|
|
246
|
+
* wallet, so this is the zero address (a documented sentinel: the account key —
|
|
247
|
+
* not an address — is the identity; the server names storage by the key's hash).
|
|
248
|
+
*/
|
|
249
|
+
/** The wallet address (its namespace) in wallet/signer mode; `undefined` in account-key mode. */
|
|
250
|
+
readonly address: `0x${string}` | undefined;
|
|
251
|
+
/** The raw `ak_…` bearer token in account-key mode; `undefined` otherwise. */
|
|
252
|
+
readonly accountKey?: string;
|
|
253
|
+
readonly endpoint: string;
|
|
254
|
+
readonly network: string;
|
|
255
|
+
readonly maxSpendUsd?: number;
|
|
256
|
+
readonly maxSessionSpendUsd?: number;
|
|
257
|
+
/** Bounded internal retries on transient failures (network error / 5xx). */
|
|
258
|
+
readonly maxRetries: number;
|
|
259
|
+
private readonly timeoutMs?;
|
|
260
|
+
private readonly fetchImpl?;
|
|
261
|
+
private _ikm?;
|
|
262
|
+
private _km?;
|
|
263
|
+
private _kmPromise?;
|
|
264
|
+
private sessionSpentUsd;
|
|
265
|
+
private readonly prepay?;
|
|
266
|
+
/** Top-off amount in atomic USDC units (1e6), computed once in the constructor. */
|
|
267
|
+
private readonly topoffAtomic;
|
|
268
|
+
/** Account-key top-off hook (account-key mode only; validated in the constructor). */
|
|
269
|
+
private readonly topoffPayer?;
|
|
270
|
+
/**
|
|
271
|
+
* Account-key inline x402 transport hook (account-key mode only; validated in
|
|
272
|
+
* the constructor). Mutually exclusive with `topoffPayer` PER OP — `topoffPayer`
|
|
273
|
+
* always takes precedence when both are configured (see the call sites in
|
|
274
|
+
* set()/get(), which gate on `!this.topoffPayer`).
|
|
275
|
+
*/
|
|
276
|
+
private readonly opInlinePayer?;
|
|
277
|
+
/** Last-known credit balance as an EXACT integer credit count (never USD floats). */
|
|
278
|
+
private knownCredits?;
|
|
279
|
+
/** Synchronous single-flight guard: at most one in-flight top-off at a time. */
|
|
280
|
+
private topoffInFlight;
|
|
281
|
+
/**
|
|
282
|
+
* The in-flight SYNCHRONOUS top-off deposit (account mode), published so a concurrent op
|
|
283
|
+
* that hits a hard 402 but can't claim the single-flight can await it and retry.
|
|
284
|
+
*/
|
|
285
|
+
private topoffPromise;
|
|
286
|
+
/** Cached last `PAYMENT-REQUIRED` header — the template for a proactive single-shot. */
|
|
287
|
+
private challengeTemplate?;
|
|
288
|
+
constructor(opts: AgentKVOptions);
|
|
289
|
+
/**
|
|
290
|
+
* Resolve (and memoize) the AES key. Async only for the sign-to-derive shape
|
|
291
|
+
* (`{signer}` with no encryptionKey): the key is `HKDF` over a fixed-message
|
|
292
|
+
* signature, which is stable ONLY for deterministic ECDSA signers (local keys /
|
|
293
|
+
* RFC-6979). Non-deterministic signers (some MPC/threshold backends) would
|
|
294
|
+
* derive a different key each run and fail to decrypt — those must pass an
|
|
295
|
+
* explicit `encryptionKey`.
|
|
296
|
+
*/
|
|
297
|
+
private getKeyMaterial;
|
|
298
|
+
/**
|
|
299
|
+
* Decrypt a value envelope with the current value key, binding the key's blind-index
|
|
300
|
+
* digest into the AAD so a value the server serves for the wrong key fails the auth tag.
|
|
301
|
+
*/
|
|
302
|
+
private decryptValue;
|
|
303
|
+
private assertSpend;
|
|
304
|
+
private recordSpend;
|
|
305
|
+
/**
|
|
306
|
+
* Reject a SERVER-QUOTED per-op price above a sane ceiling in the DEFAULT (cap-less) config.
|
|
307
|
+
* When `maxSpendUsd` is set, `assertSpend` already bounds the op price; when it is NOT set,
|
|
308
|
+
* a compromised or spoofed worker could otherwise answer a routine $0.005 read with a 402
|
|
309
|
+
* challenge for the wallet's whole balance and the client would sign the EIP-3009
|
|
310
|
+
* authorization. Callers who genuinely need a pricier op opt in via `maxSpendUsd`.
|
|
311
|
+
*/
|
|
312
|
+
private assertOpPriceCeiling;
|
|
313
|
+
/**
|
|
314
|
+
* The effective per-op ceiling (USD) for an inline-payer op: the caller's `maxSpendUsd`
|
|
315
|
+
* when set (they opted into that bound), else the built-in default ceiling. Handed to the
|
|
316
|
+
* hook as its hard `maxAmountAtomic`, and pre-reserved against the session cap before paying.
|
|
317
|
+
*/
|
|
318
|
+
private inlineOpCeilingUsd;
|
|
319
|
+
/**
|
|
320
|
+
* The on-chain settlement txHash from a response's PAYMENT-RESPONSE header, or ""
|
|
321
|
+
* when the server served the op from existing credits (so the attached top-off
|
|
322
|
+
* authorization was NEVER settled — it just expires) or the header is absent. The
|
|
323
|
+
* worker emits PAYMENT-RESPONSE = base64(JSON `{ success, payer, amount, txHash }`)
|
|
324
|
+
* on any paid 200, with `txHash: ""` on the credit hot path. A proactive single-shot
|
|
325
|
+
* top-off must only count toward session spend when this is non-empty — otherwise no
|
|
326
|
+
* USDC moved and recording it inflates sessionSpentUsd, prematurely tripping the cap.
|
|
327
|
+
*
|
|
328
|
+
* Accepted trade-off: in a doubly-rare crash window (the server's settle mined on-chain
|
|
329
|
+
* but its ledger row was lost, AND the response was lost so the client retries), the
|
|
330
|
+
* worker's already-used-authorization recovery returns success with txHash "" even
|
|
331
|
+
* though USDC did move. The client then under-counts that top-off by one, making the
|
|
332
|
+
* local session cap marginally lenient — no funds are lost (the amount is still minted
|
|
333
|
+
* as credits). This is unavoidable (the worker cannot distinguish that case from a
|
|
334
|
+
* plain credit-served op) and far cheaper than the systematic L3 over-count it replaces.
|
|
335
|
+
*/
|
|
336
|
+
private settledTxHash;
|
|
337
|
+
/**
|
|
338
|
+
* Issue a request with bounded internal retry on TRANSIENT failures only: a
|
|
339
|
+
* thrown fetch (network error / lost response) or a 5xx. `build()` is re-invoked
|
|
340
|
+
* per attempt so the credit path can re-sign identity with a FRESH nonce each
|
|
341
|
+
* time, while the op's stable Idempotency-Key (and pinned EIP-3009 nonce on paid
|
|
342
|
+
* ops) makes a retry of an already-processed request dedupe server-side — so a
|
|
343
|
+
* lost response that the server already charged is recovered without a second
|
|
344
|
+
* charge. NOT retried: any 2xx/4xx (incl. the 402 credit->pay handoff, 401, 404)
|
|
345
|
+
* — those are returned as-is for the caller's normal handling. The bound is kept
|
|
346
|
+
* small so a re-sent paid authorization cannot outlive its validBefore.
|
|
347
|
+
*
|
|
348
|
+
* The retry MECHANICS (transient-status detection, backoff,
|
|
349
|
+
* Retry-After honoring) were extracted to `@agentx402-ai/core`'s `fetchWithRetry`
|
|
350
|
+
* as a pure function parameterized by `maxRetries` — this method is now a
|
|
351
|
+
* thin delegating wrapper so every existing `this.fetchWithRetry(...)` call
|
|
352
|
+
* site above is unchanged.
|
|
353
|
+
*/
|
|
354
|
+
private fetchWithRetry;
|
|
355
|
+
/**
|
|
356
|
+
* Update prepay tracking from any server response. Reads the exact integer
|
|
357
|
+
* credit balance (`X-AgentKV-Credits-Remaining`) and caches the most recent
|
|
358
|
+
* `PAYMENT-REQUIRED` challenge as the proactive single-shot template (so a
|
|
359
|
+
* top-off needs no preflight request). Safe to call when prepay is disabled.
|
|
360
|
+
*/
|
|
361
|
+
private trackBalance;
|
|
362
|
+
/** Watermark (USD) expressed in EXACT integer credits (1 credit = CREDIT_VALUE_USD = $0.0001,
|
|
363
|
+
* so $1 = 10,000 credits — matching the worker's mint rate). */
|
|
364
|
+
private watermarkCredits;
|
|
365
|
+
/**
|
|
366
|
+
* Synchronous single-flight claim for a proactive top-off. CRITICAL: there is
|
|
367
|
+
* NO `await` between the watermark check and setting `topoffInFlight = true`,
|
|
368
|
+
* and this must be called at the very top of set/get before any await.
|
|
369
|
+
* Otherwise two concurrent ops both pass the check and each fire a separate
|
|
370
|
+
* top-off with distinct fresh nonces the server can't dedupe (double charge).
|
|
371
|
+
* Returns true for exactly one concurrent op below the watermark; the caller
|
|
372
|
+
* MUST clear `topoffInFlight` in a `finally`. Losers take the identity path.
|
|
373
|
+
*/
|
|
374
|
+
private tryClaimTopoff;
|
|
375
|
+
/**
|
|
376
|
+
* Single-flight claim at a hard 402 (insufficient credits). Unlike
|
|
377
|
+
* `tryClaimTopoff` this ignores the watermark — a 402 already proves credits
|
|
378
|
+
* are short — but still claims the flag synchronously so concurrent 402s don't
|
|
379
|
+
* each fire a top-off. Returns true only if the flag was free; the caller MUST
|
|
380
|
+
* clear it in a `finally`. In `async` mode we leave the 402 to be paid at the
|
|
381
|
+
* op price (the background deposit replenishes credits separately).
|
|
382
|
+
*/
|
|
383
|
+
private tryClaimTopoffOnFault;
|
|
384
|
+
/**
|
|
385
|
+
* Run a synchronous account-mode top-off while PUBLISHING its in-flight promise, so a
|
|
386
|
+
* concurrent op that hits a hard 402 (and can't claim the single-flight) can await THIS
|
|
387
|
+
* deposit and retry rather than surfacing the 402. The caller holds the single-flight claim.
|
|
388
|
+
*/
|
|
389
|
+
private runSharedTopoff;
|
|
390
|
+
/**
|
|
391
|
+
* Detached async top-off (opt-in via `prepay.async`). When below the watermark
|
|
392
|
+
* and no top-off is in flight, fire a deposit WITHOUT awaiting it in the op
|
|
393
|
+
* path. Documented races: the balance read is point-in-time so the trigger can
|
|
394
|
+
* be stale, and a deposit settling between an op's read and this check can make
|
|
395
|
+
* the top-off redundant; the single-flight flag bounds these to at most one
|
|
396
|
+
* outstanding deposit, but cannot serialize against an op already past its own
|
|
397
|
+
* check. Use the (default) synchronous single-shot for exactly-bounded spend.
|
|
398
|
+
*
|
|
399
|
+
* Account-key mode: there is no signing wallet, so the detached
|
|
400
|
+
* deposit is dispatched through `runAccountTopoff()` (the `topoffPayer` hook)
|
|
401
|
+
* instead of `runDeposit` (which throws `no_signer` in account mode). Wallet
|
|
402
|
+
* mode is completely unchanged below.
|
|
403
|
+
*/
|
|
404
|
+
private maybeAsyncTopoff;
|
|
405
|
+
/**
|
|
406
|
+
* Whether a top-off of `prepay.topoff` fits under the cumulative SESSION cap.
|
|
407
|
+
* Top-offs are deliberately NOT gated on the per-op `maxSpendUsd` (which bounds
|
|
408
|
+
* individual pay-per-op charges); when over the session cap we downgrade to
|
|
409
|
+
* pay-per-op rather than throwing.
|
|
410
|
+
*/
|
|
411
|
+
private topoffFitsSessionCap;
|
|
412
|
+
/**
|
|
413
|
+
* Single source of truth for BOTH the EIP-712-signed pathname (`path`) and the
|
|
414
|
+
* URL to fetch (`url`), so they can never diverge — a divergence silently
|
|
415
|
+
* breaks identity auth: the worker verifies over the RECEIVED path and
|
|
416
|
+
* recovers a phantom address if it differs from what the client signed.
|
|
417
|
+
* `base` is the un-prefixed pathname; the fetched/signed path is `/v1` + `base`
|
|
418
|
+
* (list-keys overrides it to `/v1/kv`). `query` is appended to `url`
|
|
419
|
+
* ONLY — EIP-712 binds the pathname, never the query string.
|
|
420
|
+
*/
|
|
421
|
+
private route;
|
|
422
|
+
private kvRoute;
|
|
423
|
+
/**
|
|
424
|
+
* Per-op auth headers. In account-key mode this is the `Authorization: Bearer
|
|
425
|
+
* ak_…` header (server hashes it to name storage + debit credits); in wallet
|
|
426
|
+
* mode it is the EIP-712 identity signature. Used by every op so the same call
|
|
427
|
+
* site picks the right scheme. Async to share the signature of `identityHeaders`.
|
|
428
|
+
*/
|
|
429
|
+
private authHeaders;
|
|
430
|
+
/**
|
|
431
|
+
* The signing wallet, asserted present. Every x402 path (set/get top-off + 402
|
|
432
|
+
* fallback, deposit) is gated behind `!this.accountKey` and so always has a
|
|
433
|
+
* signer; this narrows the optional type at those call sites (and fails loudly
|
|
434
|
+
* if that invariant is ever broken).
|
|
435
|
+
*/
|
|
436
|
+
private requireSigner;
|
|
437
|
+
/** EIP-712 identity headers with the deployment host bound into the signature (prevents cross-deployment signature replay). */
|
|
438
|
+
private identityHeaders;
|
|
439
|
+
/**
|
|
440
|
+
* Shared money/transport orchestrator behind set() and getInternal() — the single copy of
|
|
441
|
+
* the flow both share: account-key (bearer) mode with proactive + hard-402 top-off and the
|
|
442
|
+
* inline-payer path; wallet mode with the proactive single-shot, credit path, and 402
|
|
443
|
+
* pay-and-retry — including the single-flight top-off accounting and settled-txHash spend
|
|
444
|
+
* gating. Per-op differences (method/body, credit cost, 404 handling, success/inline
|
|
445
|
+
* parsing) come from `spec`. The CALLER must claim the single-flight top-off SYNCHRONOUSLY
|
|
446
|
+
* (before any await) and pass it in `flight`; it may be re-claimed here on a cold-start hard
|
|
447
|
+
* 402, and the caller's `finally` releases it.
|
|
448
|
+
*/
|
|
449
|
+
private performOp;
|
|
450
|
+
/**
|
|
451
|
+
* Write an encrypted value. The value is JSON-stringified and AES-256-GCM
|
|
452
|
+
* encrypted client-side; the server only ever stores ciphertext. `null` and
|
|
453
|
+
* `undefined` are rejected (`invalid_value`) so a `null` from get() unambiguously
|
|
454
|
+
* means "missing key" — use delete() to remove a key. Tries the credit path first
|
|
455
|
+
* (EIP-712 identity signature); if credits are insufficient the server returns a 402
|
|
456
|
+
* x402 challenge and the client pays. A stable Idempotency-Key is reused across that
|
|
457
|
+
* retry so the write is exactly-once.
|
|
458
|
+
*/
|
|
459
|
+
set(key: string, value: unknown, opts?: SetOptions): Promise<SetResult>;
|
|
460
|
+
/**
|
|
461
|
+
* Read and decrypt a value. Tries the credit path first (EIP-712 identity);
|
|
462
|
+
* if credits are insufficient the server returns a 402 x402 challenge and the
|
|
463
|
+
* client pays, then retries. Returns null if the key is missing or expired (404);
|
|
464
|
+
* stored values are never null (set rejects it), so null unambiguously means absent.
|
|
465
|
+
*/
|
|
466
|
+
get<T = unknown>(key: string, opts?: GetOptions): Promise<T | null>;
|
|
467
|
+
/**
|
|
468
|
+
* Like `get`, but ALSO surfaces the machine-readable usage envelope the server
|
|
469
|
+
* attaches to a paid read's success body — a separate, additive
|
|
470
|
+
* accessor so `get()` keeps its narrower `T | null` signature (no existing
|
|
471
|
+
* caller breaks). `usage` is absent when the key was missing/expired (404 —
|
|
472
|
+
* the read op itself is never charged on a miss, see the DO's 404-before-charge
|
|
473
|
+
* precheck — though a proactive top-off, if one was triggered for this call,
|
|
474
|
+
* is a separate deposit charge and can still happen alongside a miss) or when
|
|
475
|
+
* talking to a server that predates the usage envelope.
|
|
476
|
+
*/
|
|
477
|
+
getWithUsage<T = unknown>(key: string, opts?: GetOptions): Promise<{
|
|
478
|
+
value: T | null;
|
|
479
|
+
usage?: UsageBlock;
|
|
480
|
+
}>;
|
|
481
|
+
/**
|
|
482
|
+
* Shared implementation behind `get`/`getWithUsage`. Tries the credit path
|
|
483
|
+
* first (EIP-712 identity); if credits are insufficient the server returns a
|
|
484
|
+
* 402 x402 challenge and the client pays, then retries. `value` is null if
|
|
485
|
+
* the key is missing or expired (404); stored values are never null (set
|
|
486
|
+
* rejects it), so null unambiguously means absent.
|
|
487
|
+
*/
|
|
488
|
+
private getInternal;
|
|
489
|
+
/**
|
|
490
|
+
* Delete a key. Free operation. Authenticated with the account-key bearer in
|
|
491
|
+
* account mode, else an EIP-712 identity signature (fresh nonce + timestamp).
|
|
492
|
+
* The digest is computed from the local key material either way.
|
|
493
|
+
*/
|
|
494
|
+
delete(key: string): Promise<DeleteResult>;
|
|
495
|
+
/**
|
|
496
|
+
* List the wallet's keys. The server returns opaque per-wallet digests plus each key's
|
|
497
|
+
* ENCRYPTED name; this decrypts the names locally and returns them — the server never
|
|
498
|
+
* sees a plaintext key name. Free (EIP-712 identity signed). Paginated: pass the returned
|
|
499
|
+
* `cursor` to fetch the next page; `cursor` is null once exhausted.
|
|
500
|
+
*/
|
|
501
|
+
listKeys(opts?: {
|
|
502
|
+
cursor?: string | null;
|
|
503
|
+
limit?: number;
|
|
504
|
+
}): Promise<{
|
|
505
|
+
keys: string[];
|
|
506
|
+
cursor: string | null;
|
|
507
|
+
}>;
|
|
508
|
+
/**
|
|
509
|
+
* Read the pre-paid credit balance. Free. Account-key bearer in account mode,
|
|
510
|
+
* else an EIP-712 identity signature.
|
|
511
|
+
*/
|
|
512
|
+
balance(): Promise<number>;
|
|
513
|
+
/**
|
|
514
|
+
* Buy credits with an x402 payment. `amountUsd` must be at least $1; any
|
|
515
|
+
* amount is accepted (no fixed tiers). This settles on-chain once via the
|
|
516
|
+
* facilitator; the returned credits are then spendable by set/get with no
|
|
517
|
+
* further payment.
|
|
518
|
+
*/
|
|
519
|
+
deposit(amountUsd: number, opts?: {
|
|
520
|
+
idempotencyKey?: string;
|
|
521
|
+
expectedPayTo?: string;
|
|
522
|
+
}): Promise<DepositResult>;
|
|
523
|
+
/**
|
|
524
|
+
* Account-key top-off: delegate payment of `${endpoint}/account/deposit` to the
|
|
525
|
+
* configured `topoffPayer` (a managed account has no signing wallet to sign an
|
|
526
|
+
* x402 payment). The caller must hold the single-flight claim and have checked
|
|
527
|
+
* `topoffFitsSessionCap()`. On resolve (= the deposit SETTLED) the top-off is
|
|
528
|
+
* recorded against the session budget only — top-offs are credit purchases, not
|
|
529
|
+
* per-op charges, so the per-op cap is deliberately not consulted (mirrors the
|
|
530
|
+
* wallet-mode top-off budget rules). A rejection is wrapped as
|
|
531
|
+
* `account_topoff_failed`; the ak_ bearer is never included in the message.
|
|
532
|
+
*/
|
|
533
|
+
/**
|
|
534
|
+
* `amountUsd` generalizes this beyond the fixed `prepay.topoff` amount
|
|
535
|
+
* so `deposit()` can reuse it for a caller-chosen amount. OMITTED (the no-arg
|
|
536
|
+
* call from the proactive/hard-402/async paths above), it defaults to
|
|
537
|
+
* `prepay.topoff` and its precomputed `topoffAtomic` ceiling — BYTE-FOR-BYTE the
|
|
538
|
+
* stage-1 behavior. An EXPLICIT `amountUsd` (from `deposit()`, which may be
|
|
539
|
+
* called with no `prepay` configured at all) is validated here the same way
|
|
540
|
+
* `runDeposit` validates its wallet-mode amount, since it never passed through
|
|
541
|
+
* the constructor's `prepay.topoff` guard.
|
|
542
|
+
*/
|
|
543
|
+
private runAccountTopoff;
|
|
544
|
+
private runDeposit;
|
|
545
|
+
/**
|
|
546
|
+
* Fund an ACCOUNT-KEY namespace — "payer funds, bearer owns". A CALLER-supplied
|
|
547
|
+
* `signer` pays via x402 to add prepaid credits to THIS client's account (the
|
|
548
|
+
* one named by its `ak_…` bearer). The payer and the owner are deliberately
|
|
549
|
+
* DECOUPLED: the payer wallet signs the on-chain EIP-3009 authorization, while
|
|
550
|
+
* the bearer — not the payer's address — owns the credited namespace. This is the
|
|
551
|
+
* SDK counterpart of the server's `/account/deposit` route.
|
|
552
|
+
*
|
|
553
|
+
* Account-key mode ONLY. In WALLET mode the paying wallet IS the namespace, so
|
|
554
|
+
* use `deposit()` instead — calling this throws `wrong_mode` before any network.
|
|
555
|
+
*
|
|
556
|
+
* `signer` is the PAYER: a viem account (must expose `address` + `signTypedData`)
|
|
557
|
+
* or a raw `0x` private key (built into a viem account internally, mirroring the
|
|
558
|
+
* constructor). `amountUsd` must be a whole number of dollars >= $1. UNLIKE
|
|
559
|
+
* `deposit()` (which IS gated by both spend caps and counts toward session spend),
|
|
560
|
+
* this explicit funding call is NOT gated by `maxSpendUsd`/`maxSessionSpendUsd` and
|
|
561
|
+
* does not count toward session spend — the payer is an EXTERNAL wallet, not this
|
|
562
|
+
* client's tracked budget. The local encryption key is never touched (funding does not encrypt).
|
|
563
|
+
*/
|
|
564
|
+
fundAccount(signer: Signer | `0x${string}`, amountUsd: number, opts?: {
|
|
565
|
+
idempotencyKey?: string;
|
|
566
|
+
expectedPayTo?: string;
|
|
567
|
+
}): Promise<DepositResult>;
|
|
568
|
+
/** Shared by `asError` (a real `Response`) and the `opInlinePayer` path (a plain `{status,body}`). */
|
|
569
|
+
private errorFromBody;
|
|
570
|
+
private asError;
|
|
571
|
+
/**
|
|
572
|
+
* The settled amount (USD) from an `opInlinePayer` response's PAYMENT-RESPONSE
|
|
573
|
+
* header — the inline-path mirror of `settledTxHash()` above, but reading a
|
|
574
|
+
* plain `Record<string,string>` (the hook's own headers, not a `Response`) and
|
|
575
|
+
* returning the `amount` field instead of the `txHash`. Case-insensitive header
|
|
576
|
+
* lookup: an external transport (e.g. awal) is not guaranteed to preserve the
|
|
577
|
+
* worker's exact `PAYMENT-RESPONSE` casing. Returns `undefined` when the header
|
|
578
|
+
* is absent/unparsable OR the op settled nothing (served from existing credits,
|
|
579
|
+
* `txHash: ""`) — callers fall back to the credit-equivalent op price.
|
|
580
|
+
*/
|
|
581
|
+
private inlineSettledAmountUsd;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export { ACCOUNT_READ_USD, ACCOUNT_WRITE_USD, AgentKV, type AgentKVOptions, CREDIT_VALUE_USD, DEFAULT_MAX_OP_USD, type DeleteResult, type DepositResult, type ErrorBody, type GetOptions, type KeyMaterial, type OpInlineRequest, type OpInlineResponse, type SetOptions, type SetResult, type TopoffPayerRequest, VERSION, decrypt, deriveKeyMaterial, encrypt, generateAccountKey, hashKey, isAccountKeyFormat };
|