@absol-labs/agent 0.6.0 → 0.7.1

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.
Files changed (47) hide show
  1. package/README.md +21 -5
  2. package/dist/index.d.ts +6 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/wallet/autonomous-wallet-broker.d.ts +97 -0
  7. package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -0
  8. package/dist/wallet/autonomous-wallet-broker.js +478 -0
  9. package/dist/wallet/autonomous-wallet-broker.js.map +1 -0
  10. package/dist/wallet/autonomous-wallet-protocol.d.ts +56 -0
  11. package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -0
  12. package/dist/wallet/autonomous-wallet-protocol.js +15 -0
  13. package/dist/wallet/autonomous-wallet-protocol.js.map +1 -0
  14. package/dist/wallet/autonomous-wallet.d.ts +106 -0
  15. package/dist/wallet/autonomous-wallet.d.ts.map +1 -0
  16. package/dist/wallet/autonomous-wallet.js +494 -0
  17. package/dist/wallet/autonomous-wallet.js.map +1 -0
  18. package/dist/wallet/privy-broker-server-entry.d.ts +2 -0
  19. package/dist/wallet/privy-broker-server-entry.d.ts.map +1 -0
  20. package/dist/wallet/privy-broker-server-entry.js +8 -0
  21. package/dist/wallet/privy-broker-server-entry.js.map +1 -0
  22. package/dist/wallet/privy-broker-server.d.ts +29 -0
  23. package/dist/wallet/privy-broker-server.d.ts.map +1 -0
  24. package/dist/wallet/privy-broker-server.js +480 -0
  25. package/dist/wallet/privy-broker-server.js.map +1 -0
  26. package/dist/wallet/privy-session-broker.d.ts +109 -0
  27. package/dist/wallet/privy-session-broker.d.ts.map +1 -0
  28. package/dist/wallet/privy-session-broker.js +372 -0
  29. package/dist/wallet/privy-session-broker.js.map +1 -0
  30. package/dist/wallet/privy-session-provider.d.ts +21 -0
  31. package/dist/wallet/privy-session-provider.d.ts.map +1 -0
  32. package/dist/wallet/privy-session-provider.js +94 -0
  33. package/dist/wallet/privy-session-provider.js.map +1 -0
  34. package/dist/wallet/provider.d.ts +51 -2
  35. package/dist/wallet/provider.d.ts.map +1 -1
  36. package/dist/wallet/provider.js +138 -1
  37. package/dist/wallet/provider.js.map +1 -1
  38. package/package.json +3 -1
  39. package/src/index.ts +73 -0
  40. package/src/wallet/autonomous-wallet-broker.ts +774 -0
  41. package/src/wallet/autonomous-wallet-protocol.ts +71 -0
  42. package/src/wallet/autonomous-wallet.ts +779 -0
  43. package/src/wallet/privy-broker-server-entry.ts +9 -0
  44. package/src/wallet/privy-broker-server.ts +573 -0
  45. package/src/wallet/privy-session-broker.ts +634 -0
  46. package/src/wallet/privy-session-provider.ts +129 -0
  47. package/src/wallet/provider.ts +260 -3
@@ -0,0 +1,9 @@
1
+ import { startPrivyBrokerServer } from "./privy-broker-server.js";
2
+
3
+ const server = await startPrivyBrokerServer();
4
+
5
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
6
+ process.once(signal, () => {
7
+ server.close(() => process.exit(0));
8
+ });
9
+ }
@@ -0,0 +1,573 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import {
3
+ createServer,
4
+ type IncomingMessage,
5
+ type ServerResponse,
6
+ } from "node:http";
7
+ import { dirname, resolve } from "node:path";
8
+
9
+ import { PrivyClient } from "@privy-io/node";
10
+ import { MetrikClient } from "@absol-labs/sdk";
11
+ import type { Hex } from "viem";
12
+ import { z } from "zod";
13
+
14
+ import {
15
+ AutonomousWalletBroker,
16
+ AutonomousWalletBrokerError,
17
+ createPrivyAutonomousProvider,
18
+ type AutonomousPreparedRequest,
19
+ type AutonomousWalletBrokerStore,
20
+ type AutonomousWalletProvision,
21
+ } from "./autonomous-wallet-broker.js";
22
+ import {
23
+ PrivySessionBroker,
24
+ PrivySessionBrokerError,
25
+ type PrivyBrokerProvider,
26
+ type PrivyBrokerSession,
27
+ type PrivyBrokerSessionStore,
28
+ } from "./privy-session-broker.js";
29
+
30
+ const serverEnvSchema = z.object({
31
+ PRIVY_APP_ID: z.string().min(1),
32
+ PRIVY_APP_SECRET: z.string().min(1),
33
+ PRIVY_AUTHORIZATION_PRIVATE_KEY_FILE: z.string().min(1),
34
+ PRIVY_KEY_QUORUM_ID: z.string().min(1),
35
+ PRIVY_POLICY_ID: z.string().min(1),
36
+ METRIK_PRIVY_SESSION_STORE: z.string().min(1),
37
+ METRIK_PRIVY_AUTONOMOUS_STORE: z.string().min(1).optional(),
38
+ METRIK_PRIVY_BROKER_PORT: z.coerce.number().int().positive().default(8095),
39
+ METRIK_PRIVY_ALLOWED_ORIGINS: z
40
+ .string()
41
+ .default(
42
+ "https://app.metrik.live,http://localhost:5173,http://localhost:4173",
43
+ ),
44
+ });
45
+
46
+ const createSessionSchema = z.object({
47
+ walletId: z.string().min(1),
48
+ address: z.string().min(1),
49
+ signedMandate: z.unknown(),
50
+ });
51
+ const revokeSchema = z.object({ sessionId: z.string().min(1) });
52
+ const rpcSchema = z.object({
53
+ method: z.enum(["eth_sendTransaction", "eth_signTypedData_v4"]),
54
+ params: z.array(z.unknown()),
55
+ });
56
+ const autonomousProofSchema = z.object({
57
+ version: z.literal(1),
58
+ appId: z.string().min(1).max(200),
59
+ chainId: z.literal(84532),
60
+ publicKey: z.string().min(1).max(1_024),
61
+ nonce: z.string().min(16).max(256),
62
+ issuedAt: z.number().int().safe(),
63
+ signature: z.string().min(1).max(1_024),
64
+ });
65
+ const autonomousProvisionSchema = z.object({
66
+ appId: z.string().min(1).max(200),
67
+ chainId: z.literal(84532),
68
+ publicKey: z.string().min(1).max(1_024),
69
+ proof: autonomousProofSchema,
70
+ idempotencyKey: z.string().min(16).max(256),
71
+ });
72
+ const autonomousPrepareSchema = z.object({
73
+ walletId: z.string().min(1).max(256),
74
+ publicKey: z.string().min(1).max(1_024),
75
+ method: z.enum(["eth_sendTransaction", "eth_signTypedData_v4"]),
76
+ params: z.array(z.unknown()).max(4),
77
+ });
78
+ const autonomousExecuteSchema = z.object({
79
+ requestId: z.string().uuid(),
80
+ publicKey: z.string().min(1).max(1_024),
81
+ signature: z.string().min(1).max(1_024),
82
+ });
83
+
84
+ export async function startPrivyBrokerServer(
85
+ env: NodeJS.ProcessEnv = process.env,
86
+ ): Promise<ReturnType<typeof createServer>> {
87
+ const config = serverEnvSchema.parse(env);
88
+ const authorizationPrivateKey = (
89
+ await readFile(resolve(config.PRIVY_AUTHORIZATION_PRIVATE_KEY_FILE))
90
+ ).toString("base64");
91
+ const client = new PrivyClient({
92
+ appId: config.PRIVY_APP_ID,
93
+ appSecret: config.PRIVY_APP_SECRET,
94
+ });
95
+ const metrik = MetrikClient.baseSepolia();
96
+ const provider = createPrivyProvider(client);
97
+ const autonomousBroker = new AutonomousWalletBroker({
98
+ appId: config.PRIVY_APP_ID,
99
+ policyId: config.PRIVY_POLICY_ID,
100
+ provider: createPrivyAutonomousProvider(client),
101
+ store: new JsonAutonomousWalletBrokerStore(
102
+ config.METRIK_PRIVY_AUTONOMOUS_STORE ??
103
+ `${config.METRIK_PRIVY_SESSION_STORE}.autonomous`,
104
+ ),
105
+ streamReader: {
106
+ getStream: async (streamId) => {
107
+ const stream = await metrik.getStreamV2(streamId);
108
+ return {
109
+ buyer: stream.buyer,
110
+ serviceRef: stream.serviceRef,
111
+ status: stream.status,
112
+ };
113
+ },
114
+ },
115
+ });
116
+ const broker = new PrivySessionBroker({
117
+ provider,
118
+ store: new JsonPrivyBrokerSessionStore(config.METRIK_PRIVY_SESSION_STORE),
119
+ authorizationPrivateKey,
120
+ streamReader: {
121
+ getStream: async (streamId) => {
122
+ const stream = await metrik.getStreamV2(streamId);
123
+ return {
124
+ buyer: stream.buyer,
125
+ serviceRef: stream.serviceRef,
126
+ status: stream.status,
127
+ };
128
+ },
129
+ },
130
+ });
131
+ const allowedOrigins = new Set(
132
+ config.METRIK_PRIVY_ALLOWED_ORIGINS.split(",").map((value) => value.trim()),
133
+ );
134
+ const limiter = new BrokerRateLimiter();
135
+
136
+ const server = createServer(async (request, response) => {
137
+ try {
138
+ applyCors(request, response, allowedOrigins);
139
+ if (request.method === "OPTIONS") {
140
+ response.writeHead(204).end();
141
+ return;
142
+ }
143
+ const url = new URL(request.url ?? "/", "http://localhost");
144
+ if (request.method === "GET" && url.pathname === "/health") {
145
+ sendJson(response, 200, { ok: true });
146
+ return;
147
+ }
148
+ if (request.method === "GET" && url.pathname === "/v1/config") {
149
+ sendJson(response, 200, {
150
+ appId: config.PRIVY_APP_ID,
151
+ keyQuorumId: config.PRIVY_KEY_QUORUM_ID,
152
+ policyId: config.PRIVY_POLICY_ID,
153
+ chainId: 84532,
154
+ maxTotalUsdcAtomic: "1000000",
155
+ maxRatePerSecondAtomic: "1000",
156
+ maxDurationSeconds: 3600,
157
+ });
158
+ return;
159
+ }
160
+ if (request.method === "POST" && url.pathname === "/v1/sessions") {
161
+ const input = createSessionSchema.parse(await readJson(request));
162
+ const result = await broker.createSession({
163
+ accessToken: requireBearer(request),
164
+ walletId: input.walletId,
165
+ address: input.address as `0x${string}`,
166
+ signedMandate: input.signedMandate,
167
+ });
168
+ sendJson(response, 201, result);
169
+ return;
170
+ }
171
+ if (request.method === "POST" && url.pathname === "/v1/sessions/revoke") {
172
+ const input = revokeSchema.parse(await readJson(request));
173
+ await broker.revokeSession(requireBearer(request), input.sessionId);
174
+ sendJson(response, 200, { revoked: true });
175
+ return;
176
+ }
177
+ if (request.method === "POST" && url.pathname === "/v1/rpc") {
178
+ const input = rpcSchema.parse(await readJson(request));
179
+ const result = await broker.rpc({
180
+ token: requireBearer(request),
181
+ method: input.method,
182
+ params: input.params,
183
+ });
184
+ sendJson(response, 200, { result });
185
+ return;
186
+ }
187
+ if (
188
+ request.method === "POST" &&
189
+ url.pathname === "/v1/autonomous-wallets"
190
+ ) {
191
+ limiter.require(request, "provision", 5, 60 * 60);
192
+ const input = autonomousProvisionSchema.parse(await readJson(request));
193
+ const result = await autonomousBroker.provision(input);
194
+ sendJson(response, 201, result);
195
+ return;
196
+ }
197
+ if (
198
+ request.method === "POST" &&
199
+ url.pathname === "/v1/autonomous-wallets/prepare"
200
+ ) {
201
+ limiter.require(request, "prepare", 120, 60);
202
+ const input = autonomousPrepareSchema.parse(await readJson(request));
203
+ const result = await autonomousBroker.prepare(input);
204
+ sendJson(response, 201, result);
205
+ return;
206
+ }
207
+ if (
208
+ request.method === "POST" &&
209
+ url.pathname === "/v1/autonomous-wallets/execute"
210
+ ) {
211
+ limiter.require(request, "execute", 120, 60);
212
+ const input = autonomousExecuteSchema.parse(await readJson(request));
213
+ const result = await autonomousBroker.execute(input);
214
+ sendJson(response, 200, { result });
215
+ return;
216
+ }
217
+ sendJson(response, 404, { error: "not-found" });
218
+ } catch (error) {
219
+ const status = statusFor(error);
220
+ const message =
221
+ error instanceof PrivySessionBrokerError ||
222
+ error instanceof AutonomousWalletBrokerError ||
223
+ error instanceof z.ZodError
224
+ ? error.message
225
+ : "internal-error";
226
+ sendJson(response, status, { error: message });
227
+ }
228
+ });
229
+ await new Promise<void>((resolveListen, reject) => {
230
+ server.once("error", reject);
231
+ server.listen(config.METRIK_PRIVY_BROKER_PORT, "127.0.0.1", () => {
232
+ server.off("error", reject);
233
+ resolveListen();
234
+ });
235
+ });
236
+ return server;
237
+ }
238
+
239
+ export class JsonPrivyBrokerSessionStore implements PrivyBrokerSessionStore {
240
+ private queue: Promise<void> = Promise.resolve();
241
+
242
+ constructor(private readonly path: string) {}
243
+
244
+ async findByTokenHash(tokenHash: string): Promise<PrivyBrokerSession | null> {
245
+ const sessions = await this.read();
246
+ return sessions.find((session) => session.tokenHash === tokenHash) ?? null;
247
+ }
248
+
249
+ async get(id: string): Promise<PrivyBrokerSession | null> {
250
+ const sessions = await this.read();
251
+ return sessions.find((session) => session.id === id) ?? null;
252
+ }
253
+
254
+ async put(session: PrivyBrokerSession): Promise<void> {
255
+ const operation = this.queue
256
+ .catch(() => undefined)
257
+ .then(async () => {
258
+ const sessions = await this.read();
259
+ const index = sessions.findIndex(
260
+ (candidate) => candidate.id === session.id,
261
+ );
262
+ if (index === -1) sessions.push(session);
263
+ else sessions[index] = session;
264
+ const target = resolve(this.path);
265
+ const temp = `${target}.${process.pid}.tmp`;
266
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
267
+ await writeFile(temp, JSON.stringify(sessions, bigintJson), {
268
+ mode: 0o600,
269
+ });
270
+ await rename(temp, target);
271
+ });
272
+ this.queue = operation;
273
+ return operation;
274
+ }
275
+
276
+ private async read(): Promise<PrivyBrokerSession[]> {
277
+ try {
278
+ const raw = await readFile(resolve(this.path), "utf8");
279
+ return JSON.parse(raw, bigintReviver) as PrivyBrokerSession[];
280
+ } catch (error) {
281
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
282
+ throw error;
283
+ }
284
+ }
285
+ }
286
+
287
+ interface AutonomousBrokerDiskState {
288
+ readonly provisions: AutonomousWalletProvision[];
289
+ readonly prepared: AutonomousPreparedRequest[];
290
+ }
291
+
292
+ /** Persistent replay/idempotency state. It never contains an agent private key. */
293
+ export class JsonAutonomousWalletBrokerStore implements AutonomousWalletBrokerStore {
294
+ private queue: Promise<void> = Promise.resolve();
295
+
296
+ constructor(private readonly path: string) {}
297
+
298
+ async findProvisionByPublicKey(publicKey: string) {
299
+ return (
300
+ (await this.read()).provisions.find(
301
+ (value) => value.publicKey === publicKey,
302
+ ) ?? null
303
+ );
304
+ }
305
+
306
+ async findProvisionByNonce(nonce: string) {
307
+ return (
308
+ (await this.read()).provisions.find((value) => value.nonce === nonce) ??
309
+ null
310
+ );
311
+ }
312
+
313
+ async putProvision(provision: AutonomousWalletProvision): Promise<void> {
314
+ await this.mutate((state) => {
315
+ const provisions = [...state.provisions];
316
+ const index = provisions.findIndex(
317
+ (value) => value.publicKey === provision.publicKey,
318
+ );
319
+ if (index === -1) provisions.push(provision);
320
+ else provisions[index] = provision;
321
+ return { ...state, provisions };
322
+ });
323
+ }
324
+
325
+ async getPrepared(requestId: string) {
326
+ return (
327
+ (await this.read()).prepared.find(
328
+ (value) => value.requestId === requestId,
329
+ ) ?? null
330
+ );
331
+ }
332
+
333
+ async putPrepared(request: AutonomousPreparedRequest): Promise<void> {
334
+ await this.mutate((state) => {
335
+ const prepared = [...state.prepared];
336
+ const index = prepared.findIndex(
337
+ (value) => value.requestId === request.requestId,
338
+ );
339
+ if (index === -1) prepared.push(request);
340
+ else prepared[index] = request;
341
+ return { ...state, prepared };
342
+ });
343
+ }
344
+
345
+ async completePrepared(requestId: string, result: Hex, consumedAt: number) {
346
+ let completed: AutonomousPreparedRequest | null = null;
347
+ await this.mutate((state) => {
348
+ const prepared = [...state.prepared];
349
+ const index = prepared.findIndex(
350
+ (value) => value.requestId === requestId,
351
+ );
352
+ if (index === -1) return state;
353
+ const current = prepared[index]!;
354
+ completed =
355
+ current.result === undefined
356
+ ? { ...current, result, consumedAt }
357
+ : current;
358
+ prepared[index] = completed;
359
+ return { ...state, prepared };
360
+ });
361
+ return completed;
362
+ }
363
+
364
+ private async mutate(
365
+ operation: (state: AutonomousBrokerDiskState) => AutonomousBrokerDiskState,
366
+ ): Promise<void> {
367
+ const queued = this.queue
368
+ .catch(() => undefined)
369
+ .then(async () => {
370
+ const state = operation(await this.read());
371
+ const target = resolve(this.path);
372
+ const temp = `${target}.${process.pid}.tmp`;
373
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
374
+ await writeFile(temp, JSON.stringify(state), { mode: 0o600 });
375
+ await rename(temp, target);
376
+ });
377
+ this.queue = queued;
378
+ return queued;
379
+ }
380
+
381
+ private async read(): Promise<AutonomousBrokerDiskState> {
382
+ try {
383
+ const parsed = JSON.parse(
384
+ await readFile(resolve(this.path), "utf8"),
385
+ ) as AutonomousBrokerDiskState;
386
+ if (!Array.isArray(parsed.provisions) || !Array.isArray(parsed.prepared))
387
+ throw new Error("invalid autonomous broker store");
388
+ return parsed;
389
+ } catch (error) {
390
+ if ((error as NodeJS.ErrnoException).code === "ENOENT")
391
+ return { provisions: [], prepared: [] };
392
+ throw error;
393
+ }
394
+ }
395
+ }
396
+
397
+ function createPrivyProvider(client: PrivyClient): PrivyBrokerProvider {
398
+ return {
399
+ verifyAccessToken: async (token) => {
400
+ const verified = await client.utils().auth().verifyAccessToken(token);
401
+ return { userId: verified.user_id };
402
+ },
403
+ getUserWallets: async (userId) => {
404
+ const user = await client.users()._get(userId);
405
+ return user.linked_accounts.flatMap((account) => {
406
+ if (
407
+ account.type !== "wallet" ||
408
+ !("id" in account) ||
409
+ !("chain_type" in account) ||
410
+ !("address" in account)
411
+ ) {
412
+ return [];
413
+ }
414
+ return [
415
+ {
416
+ id: typeof account.id === "string" ? account.id : null,
417
+ address: account.address,
418
+ chain_type: account.chain_type,
419
+ ...(account.connector_type === undefined
420
+ ? {}
421
+ : { connector_type: String(account.connector_type) }),
422
+ ...(account.wallet_client_type === undefined
423
+ ? {}
424
+ : { wallet_client_type: String(account.wallet_client_type) }),
425
+ },
426
+ ];
427
+ });
428
+ },
429
+ sendTransaction: async (input) => {
430
+ const result = await client
431
+ .wallets()
432
+ .ethereum()
433
+ .sendTransaction(input.walletId, {
434
+ caip2: "eip155:84532",
435
+ params: { transaction: input.transaction as never },
436
+ sponsor: input.sponsor,
437
+ authorization_context: {
438
+ authorization_private_keys: [input.authorizationPrivateKey],
439
+ },
440
+ });
441
+ return result.hash as Hex;
442
+ },
443
+ signTypedData: async (input) => {
444
+ const result = await client
445
+ .wallets()
446
+ .ethereum()
447
+ .signTypedData(input.walletId, {
448
+ params: { typed_data: input.typedData as never },
449
+ authorization_context: {
450
+ authorization_private_keys: [input.authorizationPrivateKey],
451
+ },
452
+ });
453
+ return result.signature as Hex;
454
+ },
455
+ };
456
+ }
457
+
458
+ async function readJson(request: IncomingMessage): Promise<unknown> {
459
+ const chunks: Buffer[] = [];
460
+ let size = 0;
461
+ for await (const chunk of request) {
462
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
463
+ size += value.length;
464
+ if (size > 64 * 1024) {
465
+ throw new PrivySessionBrokerError("invalid-request", "request too large");
466
+ }
467
+ chunks.push(value);
468
+ }
469
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
470
+ }
471
+
472
+ function requireBearer(request: IncomingMessage): string {
473
+ const value = request.headers.authorization;
474
+ if (!value?.startsWith("Bearer ") || value.length <= 39) {
475
+ throw new PrivySessionBrokerError("unauthorized", "missing bearer token");
476
+ }
477
+ return value.slice(7);
478
+ }
479
+
480
+ function applyCors(
481
+ request: IncomingMessage,
482
+ response: ServerResponse,
483
+ allowedOrigins: ReadonlySet<string>,
484
+ ): void {
485
+ const origin = request.headers.origin;
486
+ if (origin && allowedOrigins.has(origin)) {
487
+ response.setHeader("access-control-allow-origin", origin);
488
+ response.setHeader("vary", "Origin");
489
+ response.setHeader(
490
+ "access-control-allow-headers",
491
+ "authorization,content-type",
492
+ );
493
+ response.setHeader("access-control-allow-methods", "GET,POST,OPTIONS");
494
+ }
495
+ }
496
+
497
+ function sendJson(
498
+ response: ServerResponse,
499
+ status: number,
500
+ body: unknown,
501
+ ): void {
502
+ if (response.headersSent) return;
503
+ response.writeHead(status, {
504
+ "content-type": "application/json; charset=utf-8",
505
+ "cache-control": "no-store",
506
+ "x-content-type-options": "nosniff",
507
+ });
508
+ response.end(JSON.stringify(body));
509
+ }
510
+
511
+ function statusFor(error: unknown): number {
512
+ if (error instanceof z.ZodError) return 400;
513
+ if (error instanceof AutonomousWalletBrokerError) {
514
+ if (error.code === "unauthorized") return 401;
515
+ if (error.code === "policy-denied" || error.code === "replay") return 403;
516
+ if (error.code === "invalid-request") return 400;
517
+ return 502;
518
+ }
519
+ if (!(error instanceof PrivySessionBrokerError)) return 500;
520
+ if (error.code === "unauthorized") return 401;
521
+ if (error.code === "expired" || error.code === "revoked") return 403;
522
+ if (error.code === "policy-denied") return 403;
523
+ if (error.code === "invalid-request") return 400;
524
+ return 502;
525
+ }
526
+
527
+ class BrokerRateLimiter {
528
+ private readonly buckets = new Map<
529
+ string,
530
+ { readonly startedAt: number; readonly count: number }
531
+ >();
532
+
533
+ require(
534
+ request: IncomingMessage,
535
+ scope: string,
536
+ limit: number,
537
+ windowSeconds: number,
538
+ ): void {
539
+ const now = Math.floor(Date.now() / 1_000);
540
+ const key = `${scope}:${clientAddress(request)}`;
541
+ const current = this.buckets.get(key);
542
+ if (current === undefined || now - current.startedAt >= windowSeconds) {
543
+ this.buckets.set(key, { startedAt: now, count: 1 });
544
+ return;
545
+ }
546
+ if (current.count >= limit)
547
+ throw new AutonomousWalletBrokerError(
548
+ "replay",
549
+ "autonomous wallet request rate exceeded",
550
+ );
551
+ this.buckets.set(key, { ...current, count: current.count + 1 });
552
+ }
553
+ }
554
+
555
+ function clientAddress(request: IncomingMessage): string {
556
+ const forwarded = request.headers["x-forwarded-for"];
557
+ const raw = Array.isArray(forwarded) ? forwarded.at(-1) : forwarded;
558
+ // Caddy appends the direct client as the right-most address. The server is
559
+ // loopback-only, so this header can only arrive through that local proxy.
560
+ return (
561
+ raw?.split(",").at(-1)?.trim() || request.socket.remoteAddress || "unknown"
562
+ );
563
+ }
564
+
565
+ function bigintJson(_key: string, value: unknown) {
566
+ return typeof value === "bigint" ? `${value.toString()}n` : value;
567
+ }
568
+
569
+ function bigintReviver(_key: string, value: unknown) {
570
+ return typeof value === "string" && /^\d+n$/.test(value)
571
+ ? BigInt(value.slice(0, -1))
572
+ : value;
573
+ }