@techdigger/humanode-agentlink 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2212 @@
1
+ // src/types.ts
2
+ import { z } from "zod";
3
+ var AGENTLINK = "agentlink";
4
+ var AgentLinkPayloadSchema = z.object({
5
+ domain: z.string(),
6
+ address: z.string(),
7
+ statement: z.string().optional(),
8
+ uri: z.string(),
9
+ version: z.string(),
10
+ chainId: z.string(),
11
+ type: z.enum(["eip191", "eip1271"]),
12
+ nonce: z.string(),
13
+ issuedAt: z.string(),
14
+ expirationTime: z.string().optional(),
15
+ notBefore: z.string().optional(),
16
+ requestId: z.string().optional(),
17
+ resources: z.array(z.string()).optional(),
18
+ signatureScheme: z.enum(["eip191", "eip1271"]).optional(),
19
+ signature: z.string()
20
+ });
21
+
22
+ // src/schema.ts
23
+ function buildAgentLinkSchema() {
24
+ return {
25
+ $schema: "https://json-schema.org/draft/2020-12/schema",
26
+ type: "object",
27
+ properties: {
28
+ domain: { type: "string" },
29
+ address: { type: "string" },
30
+ statement: { type: "string" },
31
+ uri: { type: "string", format: "uri" },
32
+ version: { type: "string" },
33
+ chainId: { type: "string" },
34
+ type: { type: "string" },
35
+ nonce: { type: "string" },
36
+ issuedAt: { type: "string", format: "date-time" },
37
+ expirationTime: { type: "string", format: "date-time" },
38
+ notBefore: { type: "string", format: "date-time" },
39
+ requestId: { type: "string" },
40
+ resources: { type: "array", items: { type: "string", format: "uri" } },
41
+ signature: { type: "string" }
42
+ },
43
+ required: ["domain", "address", "uri", "version", "chainId", "type", "nonce", "issuedAt", "signature"]
44
+ };
45
+ }
46
+
47
+ // src/declare.ts
48
+ function getSignatureTypes(network) {
49
+ if (!network.startsWith("eip155:")) {
50
+ throw new Error(`Biomapper-backed AgentLink only supports EVM networks. Received "${network}".`);
51
+ }
52
+ return ["eip191", "eip1271"];
53
+ }
54
+ function declareAgentLinkExtension(options = {}) {
55
+ const info = {
56
+ version: options.version ?? "1"
57
+ };
58
+ if (options.domain) {
59
+ info.domain = options.domain;
60
+ }
61
+ if (options.resourceUri) {
62
+ info.uri = options.resourceUri;
63
+ info.resources = [options.resourceUri];
64
+ }
65
+ if (options.statement) {
66
+ info.statement = options.statement;
67
+ }
68
+ let supportedChains = [];
69
+ if (options.network) {
70
+ const networks = Array.isArray(options.network) ? options.network : [options.network];
71
+ supportedChains = networks.flatMap(
72
+ (network) => getSignatureTypes(network).map((type) => ({
73
+ chainId: network,
74
+ type
75
+ }))
76
+ );
77
+ }
78
+ const declaration = {
79
+ info,
80
+ supportedChains,
81
+ schema: buildAgentLinkSchema(),
82
+ _options: options
83
+ };
84
+ return { [AGENTLINK]: declaration };
85
+ }
86
+
87
+ // src/server.ts
88
+ import { randomBytes } from "crypto";
89
+ var agentlinkResourceServerExtension = {
90
+ key: AGENTLINK,
91
+ enrichPaymentRequiredResponse: async (declaration, context) => {
92
+ const decl = declaration;
93
+ const opts = decl._options ?? {};
94
+ const resourceUri = opts.resourceUri ?? context.resourceInfo.url;
95
+ let domain = opts.domain;
96
+ if (!domain && resourceUri) {
97
+ try {
98
+ domain = new URL(resourceUri).hostname;
99
+ } catch {
100
+ }
101
+ }
102
+ let networks;
103
+ if (opts.network) {
104
+ networks = Array.isArray(opts.network) ? opts.network : [opts.network];
105
+ } else {
106
+ networks = [...new Set(context.requirements.map((r) => r.network))];
107
+ }
108
+ const nonce = randomBytes(16).toString("hex");
109
+ const issuedAt = (/* @__PURE__ */ new Date()).toISOString();
110
+ const expirationSeconds = opts.expirationSeconds;
111
+ const expirationTime = expirationSeconds !== void 0 ? new Date(Date.now() + expirationSeconds * 1e3).toISOString() : void 0;
112
+ const info = {
113
+ domain: domain ?? "",
114
+ uri: resourceUri,
115
+ version: opts.version ?? "1",
116
+ nonce,
117
+ issuedAt,
118
+ resources: [resourceUri]
119
+ };
120
+ if (expirationTime) {
121
+ info.expirationTime = expirationTime;
122
+ }
123
+ if (opts.statement) {
124
+ info.statement = opts.statement;
125
+ }
126
+ const supportedChains = networks.flatMap(
127
+ (network) => getSignatureTypes(network).map((type) => ({
128
+ chainId: network,
129
+ type
130
+ }))
131
+ );
132
+ return {
133
+ info,
134
+ supportedChains,
135
+ schema: buildAgentLinkSchema(),
136
+ ...opts.mode ? { mode: opts.mode } : {}
137
+ };
138
+ }
139
+ };
140
+
141
+ // src/parse.ts
142
+ import { Base64EncodedRegex, safeBase64Decode } from "@x402/core/utils";
143
+ function parseAgentLinkHeader(header) {
144
+ if (!Base64EncodedRegex.test(header)) {
145
+ throw new Error("Invalid agentlink header: not valid base64");
146
+ }
147
+ const jsonStr = safeBase64Decode(header);
148
+ let rawPayload;
149
+ try {
150
+ rawPayload = JSON.parse(jsonStr);
151
+ } catch (error) {
152
+ if (error instanceof SyntaxError) {
153
+ throw new Error("Invalid agentlink header: not valid JSON");
154
+ }
155
+ throw error;
156
+ }
157
+ const parsed = AgentLinkPayloadSchema.safeParse(rawPayload);
158
+ if (!parsed.success) {
159
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
160
+ throw new Error(`Invalid agentlink header: ${issues}`);
161
+ }
162
+ return parsed.data;
163
+ }
164
+
165
+ // src/validate.ts
166
+ var DEFAULT_MAX_AGE_MS = 5 * 60 * 1e3;
167
+ function normalizeComparableUrl(input) {
168
+ const url = new URL(input);
169
+ return {
170
+ host: url.host,
171
+ pathname: url.pathname,
172
+ search: url.search
173
+ };
174
+ }
175
+ async function validateAgentLinkMessage(message, expectedResourceUri, options = {}) {
176
+ const expectedUrl = new URL(expectedResourceUri);
177
+ const expectedResource = normalizeComparableUrl(expectedResourceUri);
178
+ const maxAge = options.maxAge ?? DEFAULT_MAX_AGE_MS;
179
+ if (message.domain !== expectedUrl.hostname) {
180
+ return {
181
+ valid: false,
182
+ error: `Domain mismatch: expected "${expectedUrl.hostname}", got "${message.domain}"`
183
+ };
184
+ }
185
+ let messageResource;
186
+ try {
187
+ messageResource = normalizeComparableUrl(message.uri);
188
+ } catch {
189
+ return { valid: false, error: `Invalid URI: "${message.uri}"` };
190
+ }
191
+ if (messageResource.host !== expectedResource.host) {
192
+ return {
193
+ valid: false,
194
+ error: `URI host mismatch: expected "${expectedResource.host}", got "${messageResource.host}"`
195
+ };
196
+ }
197
+ if (messageResource.pathname !== expectedResource.pathname) {
198
+ return {
199
+ valid: false,
200
+ error: `URI path mismatch: expected "${expectedResource.pathname}", got "${messageResource.pathname}"`
201
+ };
202
+ }
203
+ if (messageResource.search !== expectedResource.search) {
204
+ return {
205
+ valid: false,
206
+ error: `URI query mismatch: expected "${expectedResource.search}", got "${messageResource.search}"`
207
+ };
208
+ }
209
+ const issuedAt = new Date(message.issuedAt);
210
+ if (isNaN(issuedAt.getTime())) {
211
+ return { valid: false, error: "Invalid issuedAt timestamp" };
212
+ }
213
+ const age = Date.now() - issuedAt.getTime();
214
+ if (age > maxAge) {
215
+ return {
216
+ valid: false,
217
+ error: `Message too old: ${Math.round(age / 1e3)}s exceeds ${maxAge / 1e3}s limit`
218
+ };
219
+ }
220
+ if (age < 0) {
221
+ return { valid: false, error: "issuedAt is in the future" };
222
+ }
223
+ if (message.expirationTime) {
224
+ const expiration = new Date(message.expirationTime);
225
+ if (isNaN(expiration.getTime())) {
226
+ return { valid: false, error: "Invalid expirationTime timestamp" };
227
+ }
228
+ if (expiration < /* @__PURE__ */ new Date()) {
229
+ return { valid: false, error: "Message expired" };
230
+ }
231
+ }
232
+ if (message.notBefore) {
233
+ const notBefore = new Date(message.notBefore);
234
+ if (isNaN(notBefore.getTime())) {
235
+ return { valid: false, error: "Invalid notBefore timestamp" };
236
+ }
237
+ if (/* @__PURE__ */ new Date() < notBefore) {
238
+ return { valid: false, error: "Message not yet valid (notBefore is in the future)" };
239
+ }
240
+ }
241
+ if (options.checkNonce) {
242
+ const nonceValid = await options.checkNonce(message.nonce);
243
+ if (!nonceValid) {
244
+ return { valid: false, error: "Nonce validation failed (possible replay attack)" };
245
+ }
246
+ }
247
+ return { valid: true };
248
+ }
249
+
250
+ // src/evm.ts
251
+ import { SiweMessage } from "siwe";
252
+
253
+ // src/viem-client.ts
254
+ import * as chains from "viem/chains";
255
+ import { createPublicClient, extractChain, http } from "viem";
256
+ var allChains = Object.values(chains);
257
+ var clientCache = /* @__PURE__ */ new Map();
258
+ function getPublicClient(numericChainId, rpcUrl) {
259
+ const cacheKey = `${numericChainId}:${rpcUrl ?? ""}`;
260
+ let cached = clientCache.get(cacheKey);
261
+ if (cached) return cached;
262
+ let chain;
263
+ if (rpcUrl) {
264
+ chain = { id: numericChainId };
265
+ } else {
266
+ chain = extractChain({ chains: allChains, id: numericChainId });
267
+ }
268
+ cached = createPublicClient({ chain, transport: http(rpcUrl) });
269
+ clientCache.set(cacheKey, cached);
270
+ return cached;
271
+ }
272
+
273
+ // src/evm.ts
274
+ function extractEVMChainId(chainId) {
275
+ const match = /^eip155:(\d+)$/.exec(chainId);
276
+ if (!match) {
277
+ throw new Error(`Invalid EVM chainId format: ${chainId}. Expected eip155:<number>`);
278
+ }
279
+ return parseInt(match[1], 10);
280
+ }
281
+ function formatSIWEMessage(info, address) {
282
+ const numericChainId = extractEVMChainId(info.chainId);
283
+ const siweMessage = new SiweMessage({
284
+ domain: info.domain,
285
+ address,
286
+ statement: info.statement,
287
+ uri: info.uri,
288
+ version: info.version,
289
+ chainId: numericChainId,
290
+ nonce: info.nonce,
291
+ issuedAt: info.issuedAt,
292
+ expirationTime: info.expirationTime,
293
+ notBefore: info.notBefore,
294
+ requestId: info.requestId,
295
+ resources: info.resources
296
+ });
297
+ return siweMessage.prepareMessage();
298
+ }
299
+ async function verifyEVMSignature(message, address, signature, chainId, rpcUrl) {
300
+ const numericChainId = extractEVMChainId(chainId);
301
+ const client = getPublicClient(numericChainId, rpcUrl);
302
+ return client.verifyMessage({
303
+ address,
304
+ message,
305
+ signature
306
+ });
307
+ }
308
+
309
+ // src/verify.ts
310
+ async function verifyAgentLinkSignature(payload, rpcUrl) {
311
+ try {
312
+ if (payload.chainId.startsWith("eip155:")) {
313
+ return verifyEVMPayload(payload, rpcUrl);
314
+ }
315
+ return {
316
+ valid: false,
317
+ error: `Unsupported chain namespace: ${payload.chainId}. Biomapper-backed AgentLink currently supports EVM chains only.`
318
+ };
319
+ } catch (error) {
320
+ return {
321
+ valid: false,
322
+ error: error instanceof Error ? error.message : "Verification failed"
323
+ };
324
+ }
325
+ }
326
+ async function verifyEVMPayload(payload, rpcUrl) {
327
+ const message = formatSIWEMessage(
328
+ {
329
+ domain: payload.domain,
330
+ uri: payload.uri,
331
+ statement: payload.statement,
332
+ version: payload.version,
333
+ chainId: payload.chainId,
334
+ type: payload.type,
335
+ nonce: payload.nonce,
336
+ issuedAt: payload.issuedAt,
337
+ expirationTime: payload.expirationTime,
338
+ notBefore: payload.notBefore,
339
+ requestId: payload.requestId,
340
+ resources: payload.resources
341
+ },
342
+ payload.address
343
+ );
344
+ try {
345
+ const valid = await verifyEVMSignature(message, payload.address, payload.signature, payload.chainId, rpcUrl);
346
+ if (!valid) {
347
+ return {
348
+ valid: false,
349
+ error: `Signature verification failed. The signature does not match the reconstructed SIWE message. Ensure your agent signs exactly this message using EIP-191 (EOA) or ERC-1271 (smart wallet):
350
+
351
+ ${message}`
352
+ };
353
+ }
354
+ return { valid: true, address: payload.address };
355
+ } catch (error) {
356
+ const reason = error instanceof Error ? error.message : "Unknown error";
357
+ return {
358
+ valid: false,
359
+ error: `Signature verification error: ${reason}. The SIWE message the server reconstructed from your payload:
360
+
361
+ ${message}`
362
+ };
363
+ }
364
+ }
365
+
366
+ // src/biomapper-registry.ts
367
+ import { zeroAddress } from "viem";
368
+ var BASE_MAINNET = "eip155:8453";
369
+ var BASE_SEPOLIA = "eip155:84532";
370
+ var KNOWN_DEPLOYMENTS = {};
371
+ var BIOMAPPER_AGENT_REGISTRY_ABI = [
372
+ {
373
+ inputs: [{ internalType: "address", name: "agent", type: "address" }],
374
+ name: "agentNonce",
375
+ outputs: [{ internalType: "uint256", name: "nonce", type: "uint256" }],
376
+ stateMutability: "view",
377
+ type: "function"
378
+ },
379
+ {
380
+ inputs: [{ internalType: "address", name: "agent", type: "address" }],
381
+ name: "getAgentStatus",
382
+ outputs: [
383
+ { internalType: "address", name: "owner", type: "address" },
384
+ { internalType: "uint256", name: "generationPtr", type: "uint256" },
385
+ { internalType: "bool", name: "active", type: "bool" }
386
+ ],
387
+ stateMutability: "view",
388
+ type: "function"
389
+ },
390
+ {
391
+ inputs: [{ internalType: "address", name: "agent", type: "address" }],
392
+ name: "linkedOwner",
393
+ outputs: [{ internalType: "address", name: "owner", type: "address" }],
394
+ stateMutability: "view",
395
+ type: "function"
396
+ },
397
+ {
398
+ inputs: [
399
+ { internalType: "address", name: "agent", type: "address" },
400
+ { internalType: "uint256", name: "deadline", type: "uint256" },
401
+ { internalType: "bytes", name: "signature", type: "bytes" }
402
+ ],
403
+ name: "linkAgent",
404
+ outputs: [],
405
+ stateMutability: "nonpayable",
406
+ type: "function"
407
+ }
408
+ ];
409
+ function createBiomapperRegistryVerifier(options = {}) {
410
+ function resolveLookupChainId(chainId) {
411
+ if (options.network === "base") return BASE_MAINNET;
412
+ if (options.network === "base-sepolia") return BASE_SEPOLIA;
413
+ if (chainId === BASE_SEPOLIA) return BASE_SEPOLIA;
414
+ return BASE_MAINNET;
415
+ }
416
+ function getClient2(chainId) {
417
+ if (options.client) return options.client;
418
+ const lookupChainId = options.contractAddress && options.rpcUrl && !options.network ? chainId : resolveLookupChainId(chainId);
419
+ return getPublicClient(extractEVMChainId(lookupChainId), options.rpcUrl);
420
+ }
421
+ function getContractAddress(chainId) {
422
+ if (options.contractAddress) return options.contractAddress;
423
+ return KNOWN_DEPLOYMENTS[resolveLookupChainId(chainId)] ?? null;
424
+ }
425
+ return {
426
+ async getAgentStatus(address, chainId) {
427
+ if (!chainId.startsWith("eip155:")) return null;
428
+ const contractAddress = getContractAddress(chainId);
429
+ if (!contractAddress) return null;
430
+ const client = getClient2(chainId);
431
+ try {
432
+ const [owner, generationPtr, active] = await client.readContract({
433
+ address: contractAddress,
434
+ abi: BIOMAPPER_AGENT_REGISTRY_ABI,
435
+ functionName: "getAgentStatus",
436
+ args: [address]
437
+ });
438
+ return {
439
+ owner: owner === zeroAddress ? null : owner,
440
+ generationPtr,
441
+ active
442
+ };
443
+ } catch {
444
+ return null;
445
+ }
446
+ }
447
+ };
448
+ }
449
+
450
+ // src/biomapper.ts
451
+ var BRIDGED_BIOMAPPER_ADDRESSES = {
452
+ base: "0x31e98F489ad65dF5Ee43CBe06e4f35557Cd0abb2",
453
+ "base-sepolia": "0x16F2a7AC67B6aC1E57dD5528A24b1fC689902Be2"
454
+ };
455
+ var BIOMAPPER_APP_URLS = {
456
+ base: "https://mainnet.biomapper.hmnd.app/",
457
+ "base-sepolia": "https://testnet5.biomapper.hmnd.app/"
458
+ };
459
+ var BRIDGED_BIOMAPPER_READ_ABI = [
460
+ {
461
+ inputs: [],
462
+ name: "generationsHead",
463
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
464
+ stateMutability: "view",
465
+ type: "function"
466
+ },
467
+ {
468
+ inputs: [
469
+ { internalType: "address", name: "owner", type: "address" },
470
+ { internalType: "uint256", name: "generationPtr", type: "uint256" }
471
+ ],
472
+ name: "lookupBiomappingPtr",
473
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
474
+ stateMutability: "view",
475
+ type: "function"
476
+ }
477
+ ];
478
+
479
+ // src/biomapper-query.ts
480
+ import { z as z2 } from "zod";
481
+ import { zeroAddress as zeroAddress2 } from "viem";
482
+ var BiomapperNetworkSchema = z2.enum(["base", "base-sepolia"]);
483
+ var BiomapperAddressSchema = z2.string().regex(/^0x[a-fA-F0-9]{40}$/, "Expected a 20-byte EVM address.");
484
+ var BiomapperRpcUrlSchema = z2.string().url();
485
+ var NETWORK_CHAIN_IDS = {
486
+ base: 8453,
487
+ "base-sepolia": 84532
488
+ };
489
+ var NETWORK_CAIP2_IDS = {
490
+ base: "eip155:8453",
491
+ "base-sepolia": "eip155:84532"
492
+ };
493
+ var CHECK_AGENT_STATUS_TOOL_NAME = "check_agent_status";
494
+ var GET_CURRENT_GENERATION_TOOL_NAME = "get_current_generation";
495
+ var GET_BIOMAPPER_INFO_TOOL_NAME = "get_biomapper_info";
496
+ var CHECK_AGENT_STATUS_TOOL_DESCRIPTION = "Check if an agent wallet is linked to a biomapped human and whether the link is currently active. Returns the owner address, current Biomapper generation pointer, and active status.";
497
+ var GET_CURRENT_GENERATION_TOOL_DESCRIPTION = "Get the current Biomapper generation pointer from the Bridged Biomapper contract. Generations represent verification periods \u2014 when a generation ends, usage quotas reset.";
498
+ var GET_BIOMAPPER_INFO_TOOL_DESCRIPTION = "Get Biomapper network metadata \u2014 contract addresses, app URLs, and supported networks. Useful for discovering where to point registry queries or where users can verify their identity.";
499
+ var CheckAgentStatusInputSchema = z2.object({
500
+ agentAddress: BiomapperAddressSchema.describe("The agent wallet address (0x...)"),
501
+ network: BiomapperNetworkSchema.optional().describe("The network to query"),
502
+ registryAddress: BiomapperAddressSchema.optional().describe("The BiomapperAgentRegistry contract address (0x...)"),
503
+ rpcUrl: BiomapperRpcUrlSchema.optional().describe("Custom RPC URL (uses public RPC if omitted)")
504
+ });
505
+ var GetCurrentGenerationInputSchema = z2.object({
506
+ network: BiomapperNetworkSchema.optional().describe("The network to query"),
507
+ rpcUrl: BiomapperRpcUrlSchema.optional().describe("Custom RPC URL (uses public RPC if omitted)")
508
+ });
509
+ var GetBiomapperInfoInputSchema = z2.object({
510
+ network: BiomapperNetworkSchema.optional().describe("Specific network (returns all if omitted)")
511
+ });
512
+ var BiomapperNetworkInfoSchema = z2.object({
513
+ network: BiomapperNetworkSchema,
514
+ bridgedBiomapper: BiomapperAddressSchema,
515
+ biomapperAppUrl: z2.string().url(),
516
+ chainId: z2.number().int().positive(),
517
+ caip2: z2.string()
518
+ });
519
+ var CheckAgentStatusResultSchema = z2.object({
520
+ linked: z2.boolean(),
521
+ active: z2.boolean(),
522
+ agentAddress: BiomapperAddressSchema,
523
+ owner: BiomapperAddressSchema.nullable(),
524
+ generationPtr: z2.string().nullable(),
525
+ network: BiomapperNetworkSchema,
526
+ message: z2.string()
527
+ });
528
+ var GetCurrentGenerationResultSchema = z2.object({
529
+ generationsHead: z2.string(),
530
+ network: BiomapperNetworkSchema,
531
+ bridgedBiomapper: BiomapperAddressSchema,
532
+ biomapperAppUrl: z2.string().url()
533
+ });
534
+ var GetBiomapperInfoResultSchema = z2.object({
535
+ networks: z2.array(BiomapperNetworkInfoSchema)
536
+ });
537
+ var BiomapperQueryError = class extends Error {
538
+ constructor(code, message, options) {
539
+ super(message);
540
+ this.code = code;
541
+ this.name = "BiomapperQueryError";
542
+ if (options?.cause !== void 0) {
543
+ ;
544
+ this.cause = options.cause;
545
+ }
546
+ }
547
+ };
548
+ function getErrorMessage(error) {
549
+ return error instanceof Error ? error.message : String(error);
550
+ }
551
+ function createValidationError(method, result) {
552
+ return new BiomapperQueryError("invalid_input", `${method} received invalid input: ${result.error.message}`, {
553
+ cause: result.error
554
+ });
555
+ }
556
+ function resolveNetworkInfo(network) {
557
+ return {
558
+ network,
559
+ bridgedBiomapper: BRIDGED_BIOMAPPER_ADDRESSES[network],
560
+ biomapperAppUrl: BIOMAPPER_APP_URLS[network],
561
+ chainId: NETWORK_CHAIN_IDS[network],
562
+ caip2: NETWORK_CAIP2_IDS[network]
563
+ };
564
+ }
565
+ function createBiomapperQueryClient(defaults = {}) {
566
+ const parsedDefaults = z2.object({
567
+ network: BiomapperNetworkSchema.optional(),
568
+ registryAddress: BiomapperAddressSchema.optional(),
569
+ rpcUrl: BiomapperRpcUrlSchema.optional()
570
+ }).safeParse({
571
+ network: defaults.network,
572
+ registryAddress: defaults.registryAddress,
573
+ rpcUrl: defaults.rpcUrl
574
+ });
575
+ if (!parsedDefaults.success) {
576
+ throw createValidationError("createBiomapperQueryClient", parsedDefaults);
577
+ }
578
+ function getClient2(network, rpcUrl) {
579
+ if (defaults.client) return defaults.client;
580
+ return getPublicClient(NETWORK_CHAIN_IDS[network], rpcUrl ?? defaults.rpcUrl);
581
+ }
582
+ function resolveRequiredNetwork(network) {
583
+ const resolvedNetwork = network ?? defaults.network;
584
+ if (!resolvedNetwork) {
585
+ throw new BiomapperQueryError(
586
+ "missing_network",
587
+ "Biomapper network is required. Pass `network` to this call or set a default in createBiomapperQueryClient(...)."
588
+ );
589
+ }
590
+ return resolvedNetwork;
591
+ }
592
+ function resolveRegistryAddress(registryAddress) {
593
+ const resolvedRegistryAddress = registryAddress ?? defaults.registryAddress;
594
+ if (!resolvedRegistryAddress) {
595
+ throw new BiomapperQueryError(
596
+ "missing_registry",
597
+ "Biomapper registry address is required for agent status queries. Pass `registryAddress` to this call or set a default in createBiomapperQueryClient(...)."
598
+ );
599
+ }
600
+ return resolvedRegistryAddress;
601
+ }
602
+ return {
603
+ async checkAgentStatus(input) {
604
+ const parsed = CheckAgentStatusInputSchema.safeParse(input);
605
+ if (!parsed.success) {
606
+ throw createValidationError("checkAgentStatus", parsed);
607
+ }
608
+ const network = resolveRequiredNetwork(parsed.data.network);
609
+ const registryAddress = resolveRegistryAddress(parsed.data.registryAddress);
610
+ const client = getClient2(network, parsed.data.rpcUrl);
611
+ try {
612
+ const [owner, generationPtr, active] = await client.readContract({
613
+ address: registryAddress,
614
+ abi: BIOMAPPER_AGENT_REGISTRY_ABI,
615
+ functionName: "getAgentStatus",
616
+ args: [parsed.data.agentAddress]
617
+ });
618
+ if (owner === zeroAddress2) {
619
+ return CheckAgentStatusResultSchema.parse({
620
+ linked: false,
621
+ active: false,
622
+ agentAddress: parsed.data.agentAddress,
623
+ owner: null,
624
+ generationPtr: null,
625
+ network,
626
+ message: "This agent is not linked to any owner."
627
+ });
628
+ }
629
+ return CheckAgentStatusResultSchema.parse({
630
+ linked: true,
631
+ active,
632
+ agentAddress: parsed.data.agentAddress,
633
+ owner,
634
+ generationPtr: generationPtr.toString(),
635
+ network,
636
+ message: active ? "Agent is linked and active \u2014 the owner is biomapped in the current generation." : "Agent is linked but inactive \u2014 the owner needs to re-verify with Biomapper for the current generation."
637
+ });
638
+ } catch (error) {
639
+ throw new BiomapperQueryError(
640
+ "query_failed",
641
+ `Failed to query Biomapper agent status: ${getErrorMessage(error)}`,
642
+ { cause: error }
643
+ );
644
+ }
645
+ },
646
+ async getCurrentGeneration(input = {}) {
647
+ const parsed = GetCurrentGenerationInputSchema.safeParse(input);
648
+ if (!parsed.success) {
649
+ throw createValidationError("getCurrentGeneration", parsed);
650
+ }
651
+ const network = resolveRequiredNetwork(parsed.data.network);
652
+ const client = getClient2(network, parsed.data.rpcUrl);
653
+ const info = resolveNetworkInfo(network);
654
+ try {
655
+ const generationsHead = await client.readContract({
656
+ address: info.bridgedBiomapper,
657
+ abi: BRIDGED_BIOMAPPER_READ_ABI,
658
+ functionName: "generationsHead"
659
+ });
660
+ return GetCurrentGenerationResultSchema.parse({
661
+ generationsHead: generationsHead.toString(),
662
+ network,
663
+ bridgedBiomapper: info.bridgedBiomapper,
664
+ biomapperAppUrl: info.biomapperAppUrl
665
+ });
666
+ } catch (error) {
667
+ throw new BiomapperQueryError(
668
+ "query_failed",
669
+ `Failed to query Biomapper generation: ${getErrorMessage(error)}`,
670
+ { cause: error }
671
+ );
672
+ }
673
+ },
674
+ async getBiomapperInfo(input = {}) {
675
+ const parsed = GetBiomapperInfoInputSchema.safeParse(input);
676
+ if (!parsed.success) {
677
+ throw createValidationError("getBiomapperInfo", parsed);
678
+ }
679
+ const resolvedNetwork = parsed.data.network ?? defaults.network;
680
+ const networks = resolvedNetwork ? [resolvedNetwork] : [...BiomapperNetworkSchema.options];
681
+ return GetBiomapperInfoResultSchema.parse({
682
+ networks: networks.map((network) => resolveNetworkInfo(network))
683
+ });
684
+ }
685
+ };
686
+ }
687
+
688
+ // src/link-consent.ts
689
+ import { base, baseSepolia } from "viem/chains";
690
+ import { privateKeyToAccount } from "viem/accounts";
691
+ import { createPublicClient as createPublicClient2, http as http2 } from "viem";
692
+ var BIOMAPPER_AGENT_REGISTRY_NAME = "BiomapperAgentRegistry";
693
+ var BIOMAPPER_AGENT_REGISTRY_VERSION = "1";
694
+ var AGENT_LINK_TYPES = {
695
+ AgentLink: [
696
+ { name: "agent", type: "address" },
697
+ { name: "owner", type: "address" },
698
+ { name: "nonce", type: "uint256" },
699
+ { name: "deadline", type: "uint256" }
700
+ ]
701
+ };
702
+ var LINK_CONSENT_NETWORKS = {
703
+ base,
704
+ "base-sepolia": baseSepolia
705
+ };
706
+ var BIOMAPPER_AGENT_REGISTRY_ABI2 = [
707
+ {
708
+ inputs: [{ internalType: "address", name: "agent", type: "address" }],
709
+ name: "agentNonce",
710
+ outputs: [{ internalType: "uint256", name: "nonce", type: "uint256" }],
711
+ stateMutability: "view",
712
+ type: "function"
713
+ }
714
+ ];
715
+ function buildAgentLinkTypedData(input) {
716
+ return {
717
+ domain: {
718
+ name: BIOMAPPER_AGENT_REGISTRY_NAME,
719
+ version: BIOMAPPER_AGENT_REGISTRY_VERSION,
720
+ chainId: input.chainId,
721
+ verifyingContract: input.registry
722
+ },
723
+ types: AGENT_LINK_TYPES,
724
+ primaryType: "AgentLink",
725
+ message: {
726
+ agent: input.agent,
727
+ owner: input.owner,
728
+ nonce: input.nonce,
729
+ deadline: input.deadline
730
+ }
731
+ };
732
+ }
733
+ function resolveSigner(options) {
734
+ if (options.signer) return options.signer;
735
+ if (options.privateKey) return privateKeyToAccount(options.privateKey);
736
+ throw new Error("Missing link consent signer. Pass signer or privateKey.");
737
+ }
738
+ function getClient(options) {
739
+ if (options.client) return options.client;
740
+ const chain = LINK_CONSENT_NETWORKS[options.network];
741
+ return createPublicClient2({
742
+ chain,
743
+ transport: http2(options.rpcUrl)
744
+ });
745
+ }
746
+ async function createAgentLinkConsent(options) {
747
+ const signer = resolveSigner(options);
748
+ const chain = LINK_CONSENT_NETWORKS[options.network];
749
+ const client = getClient(options);
750
+ if (options.rpcUrl && !options.client) {
751
+ const remoteChainId = await client.getChainId?.();
752
+ if (remoteChainId === void 0) {
753
+ throw new Error("The configured consent client cannot report its chain id.");
754
+ }
755
+ if (remoteChainId !== chain.id) {
756
+ throw new Error(
757
+ `RPC chain mismatch. ${options.network} expects chain id ${chain.id}, but ${options.rpcUrl} returned ${remoteChainId}.`
758
+ );
759
+ }
760
+ }
761
+ const nonce = await client.readContract({
762
+ address: options.registry,
763
+ abi: BIOMAPPER_AGENT_REGISTRY_ABI2,
764
+ functionName: "agentNonce",
765
+ args: [signer.address]
766
+ });
767
+ const typedData = buildAgentLinkTypedData({
768
+ agent: signer.address,
769
+ owner: options.owner,
770
+ registry: options.registry,
771
+ chainId: chain.id,
772
+ nonce,
773
+ deadline: options.deadline
774
+ });
775
+ const signature = await signer.signTypedData(typedData);
776
+ return {
777
+ type: "biomapper-agent-link",
778
+ network: options.network,
779
+ chainId: chain.id,
780
+ agent: signer.address,
781
+ owner: options.owner,
782
+ registry: options.registry,
783
+ nonce: nonce.toString(),
784
+ deadline: options.deadline.toString(),
785
+ typedData: {
786
+ domain: {
787
+ name: typedData.domain.name ?? BIOMAPPER_AGENT_REGISTRY_NAME,
788
+ version: typedData.domain.version ?? BIOMAPPER_AGENT_REGISTRY_VERSION,
789
+ chainId: typedData.domain.chainId,
790
+ verifyingContract: typedData.domain.verifyingContract
791
+ },
792
+ types: typedData.types,
793
+ primaryType: typedData.primaryType,
794
+ message: {
795
+ agent: typedData.message.agent,
796
+ owner: typedData.message.owner,
797
+ nonce: typedData.message.nonce.toString(),
798
+ deadline: typedData.message.deadline.toString()
799
+ }
800
+ },
801
+ signature
802
+ };
803
+ }
804
+
805
+ // src/storage.ts
806
+ function buildAgentLinkUsageKey(input) {
807
+ return [
808
+ input.endpoint,
809
+ input.generationPtr.toString(),
810
+ input.owner.toLowerCase(),
811
+ input.platformAccountId ?? "*",
812
+ input.workspaceId ?? "*"
813
+ ].join(":");
814
+ }
815
+ var InMemoryAgentLinkStorage = class {
816
+ constructor() {
817
+ this.usage = /* @__PURE__ */ new Map();
818
+ this.nonces = /* @__PURE__ */ new Set();
819
+ }
820
+ async getUsageCount(endpoint, owner, generationPtr, context) {
821
+ return this.usage.get(
822
+ buildAgentLinkUsageKey({
823
+ endpoint,
824
+ owner,
825
+ generationPtr,
826
+ platformAccountId: context?.platformAccountId,
827
+ workspaceId: context?.workspaceId
828
+ })
829
+ ) ?? 0;
830
+ }
831
+ async incrementUsage(endpoint, owner, generationPtr, context) {
832
+ const key = buildAgentLinkUsageKey({
833
+ endpoint,
834
+ owner,
835
+ generationPtr,
836
+ platformAccountId: context?.platformAccountId,
837
+ workspaceId: context?.workspaceId
838
+ });
839
+ this.usage.set(key, (this.usage.get(key) ?? 0) + 1);
840
+ }
841
+ async hasUsedNonce(nonce) {
842
+ return this.nonces.has(nonce);
843
+ }
844
+ async recordNonce(nonce) {
845
+ this.nonces.add(nonce);
846
+ }
847
+ };
848
+
849
+ // src/hooks.ts
850
+ function createAgentLinkHooks(options) {
851
+ const { registry, onEvent } = options;
852
+ const mode = options.mode ?? { type: "free" };
853
+ const storage = options.storage;
854
+ const usageThresholds = normalizeThresholds(options.usageThresholds);
855
+ assertReplayStorage(storage);
856
+ if (mode.type === "free-trial" || mode.type === "discount") {
857
+ assertUsageStorage(storage, mode.type);
858
+ }
859
+ if (mode.type === "discount" && (!Number.isInteger(mode.percent) || mode.percent <= 0 || mode.percent > 100)) {
860
+ throw new Error(`Discount percent must be an integer between 1 and 100, got ${mode.percent}`);
861
+ }
862
+ const PENDING_TTL_MS = 5 * 60 * 1e3;
863
+ const pendingDiscounts = /* @__PURE__ */ new Map();
864
+ const observedLinks = /* @__PURE__ */ new Map();
865
+ async function emit(event) {
866
+ if (!onEvent) return;
867
+ try {
868
+ await onEvent(event);
869
+ } catch {
870
+ }
871
+ }
872
+ function getLinkedOnlyAbort() {
873
+ return {
874
+ abort: true,
875
+ reason: mode.type === "linked-only" ? mode.reason ?? "Linked agent access is required for this resource." : ""
876
+ };
877
+ }
878
+ async function resolveEntitlements(input) {
879
+ const resolved = typeof options.entitlements === "function" ? await options.entitlements(input) : options.entitlements ?? {};
880
+ return {
881
+ endpoint: resolved?.endpoint ?? input.request.path,
882
+ platformAccountId: resolved?.platformAccountId,
883
+ workspaceId: resolved?.workspaceId
884
+ };
885
+ }
886
+ function buildScopedContext(resource, entitlements) {
887
+ return {
888
+ resource,
889
+ endpoint: entitlements.endpoint,
890
+ platformAccountId: entitlements.platformAccountId,
891
+ workspaceId: entitlements.workspaceId
892
+ };
893
+ }
894
+ async function observeLinkStatus(address, status, scoped) {
895
+ if (!status) return;
896
+ const key = [address.toLowerCase(), scoped.platformAccountId ?? "*", scoped.workspaceId ?? "*"].join(":");
897
+ const previous = observedLinks.get(key);
898
+ const owner = status.owner ? status.owner.toLowerCase() : null;
899
+ if (status.owner && status.active) {
900
+ if (!previous || !previous.active || previous.owner?.toLowerCase() !== owner || previous.generationPtr !== status.generationPtr) {
901
+ await emit({
902
+ type: "link.activated",
903
+ address,
904
+ owner: status.owner,
905
+ generationPtr: status.generationPtr,
906
+ ...scoped
907
+ });
908
+ }
909
+ } else if (!status.owner) {
910
+ if (previous?.owner) {
911
+ await emit({
912
+ type: "link.unlinked",
913
+ address,
914
+ previousOwner: previous.owner,
915
+ generationPtr: previous.generationPtr,
916
+ ...scoped
917
+ });
918
+ }
919
+ } else if (previous?.owner && previous.active && !status.active) {
920
+ await emit({
921
+ type: "link.deactivated",
922
+ address,
923
+ owner: status.owner,
924
+ generationPtr: status.generationPtr,
925
+ reason: "inactive",
926
+ ...scoped
927
+ });
928
+ await emit({
929
+ type: "relink.required",
930
+ address,
931
+ owner: status.owner,
932
+ generationPtr: status.generationPtr,
933
+ reason: "inactive_owner",
934
+ ...scoped
935
+ });
936
+ }
937
+ observedLinks.set(key, {
938
+ owner: status.owner,
939
+ generationPtr: status.generationPtr,
940
+ active: status.active
941
+ });
942
+ }
943
+ async function maybeEmitThresholdEvent(input) {
944
+ const thresholds = new Set(usageThresholds);
945
+ if (input.defaultThreshold && input.defaultThreshold > 0) {
946
+ thresholds.add(input.defaultThreshold);
947
+ }
948
+ if (!thresholds.has(input.count)) return;
949
+ await emit({
950
+ type: "usage.threshold_reached",
951
+ resource: input.resource,
952
+ endpoint: input.endpoint,
953
+ address: input.address,
954
+ owner: input.owner,
955
+ generationPtr: input.generationPtr,
956
+ count: input.count,
957
+ threshold: input.count,
958
+ mode: input.mode,
959
+ platformAccountId: input.usageContext.platformAccountId,
960
+ workspaceId: input.usageContext.workspaceId
961
+ });
962
+ }
963
+ function prunePendingDiscounts(now) {
964
+ for (const [address, entries] of pendingDiscounts) {
965
+ const activeEntries = entries.filter((entry) => now - entry.createdAt <= PENDING_TTL_MS);
966
+ if (activeEntries.length > 0) {
967
+ pendingDiscounts.set(address, activeEntries);
968
+ } else {
969
+ pendingDiscounts.delete(address);
970
+ }
971
+ }
972
+ }
973
+ const requestHook = async (context) => {
974
+ const header = context.adapter.getHeader(AGENTLINK) || context.adapter.getHeader(AGENTLINK.toLowerCase());
975
+ if (!header) {
976
+ if (mode.type === "linked-only") {
977
+ return getLinkedOnlyAbort();
978
+ }
979
+ return;
980
+ }
981
+ try {
982
+ const payload = parseAgentLinkHeader(header);
983
+ const resourceUri = context.adapter.getUrl();
984
+ const checkNonce = async (nonce) => !await storage.hasUsedNonce(nonce);
985
+ const validation = await validateAgentLinkMessage(payload, resourceUri, {
986
+ checkNonce
987
+ });
988
+ if (!validation.valid) {
989
+ await emit({
990
+ type: "validation_failed",
991
+ resource: context.path,
992
+ endpoint: context.path,
993
+ error: validation.error
994
+ });
995
+ if (mode.type === "linked-only") {
996
+ return getLinkedOnlyAbort();
997
+ }
998
+ return;
999
+ }
1000
+ const verification = await verifyAgentLinkSignature(payload, options.rpcUrl);
1001
+ if (!verification.valid || !verification.address) {
1002
+ await emit({
1003
+ type: "validation_failed",
1004
+ resource: context.path,
1005
+ endpoint: context.path,
1006
+ error: verification.error
1007
+ });
1008
+ if (mode.type === "linked-only") {
1009
+ return getLinkedOnlyAbort();
1010
+ }
1011
+ return;
1012
+ }
1013
+ await storage.recordNonce(payload.nonce);
1014
+ const status = await registry.getAgentStatus(verification.address, payload.chainId);
1015
+ const entitlements = await resolveEntitlements({
1016
+ request: context,
1017
+ payload,
1018
+ address: verification.address,
1019
+ status
1020
+ });
1021
+ const scoped = buildScopedContext(context.path, entitlements);
1022
+ const usageContext = {
1023
+ platformAccountId: entitlements.platformAccountId,
1024
+ workspaceId: entitlements.workspaceId
1025
+ };
1026
+ await observeLinkStatus(verification.address, status, scoped);
1027
+ if (!status?.owner || !status.active) {
1028
+ await emit({
1029
+ type: "agent_not_verified",
1030
+ resource: context.path,
1031
+ endpoint: entitlements.endpoint,
1032
+ address: verification.address,
1033
+ platformAccountId: entitlements.platformAccountId,
1034
+ workspaceId: entitlements.workspaceId
1035
+ });
1036
+ if (mode.type === "linked-only") {
1037
+ return getLinkedOnlyAbort();
1038
+ }
1039
+ return;
1040
+ }
1041
+ if (mode.type === "linked-only" || mode.type === "free") {
1042
+ await emit({
1043
+ type: "agent_verified",
1044
+ resource: context.path,
1045
+ endpoint: entitlements.endpoint,
1046
+ address: verification.address,
1047
+ owner: status.owner,
1048
+ generationPtr: status.generationPtr,
1049
+ platformAccountId: entitlements.platformAccountId,
1050
+ workspaceId: entitlements.workspaceId
1051
+ });
1052
+ return { grantAccess: true };
1053
+ }
1054
+ if (mode.type === "free-trial") {
1055
+ const usageStorage = assertUsageStorage(storage, mode.type);
1056
+ const uses = mode.uses ?? 1;
1057
+ const count = await usageStorage.getUsageCount(
1058
+ entitlements.endpoint,
1059
+ status.owner,
1060
+ status.generationPtr,
1061
+ usageContext
1062
+ );
1063
+ if (count < uses) {
1064
+ const nextCount = count + 1;
1065
+ await usageStorage.incrementUsage(
1066
+ entitlements.endpoint,
1067
+ status.owner,
1068
+ status.generationPtr,
1069
+ usageContext
1070
+ );
1071
+ await emit({
1072
+ type: "agent_verified",
1073
+ resource: context.path,
1074
+ endpoint: entitlements.endpoint,
1075
+ address: verification.address,
1076
+ owner: status.owner,
1077
+ generationPtr: status.generationPtr,
1078
+ platformAccountId: entitlements.platformAccountId,
1079
+ workspaceId: entitlements.workspaceId
1080
+ });
1081
+ await maybeEmitThresholdEvent({
1082
+ resource: context.path,
1083
+ endpoint: entitlements.endpoint,
1084
+ address: verification.address,
1085
+ owner: status.owner,
1086
+ generationPtr: status.generationPtr,
1087
+ mode: "free-trial",
1088
+ usageContext,
1089
+ count: nextCount,
1090
+ defaultThreshold: uses
1091
+ });
1092
+ return { grantAccess: true };
1093
+ }
1094
+ return;
1095
+ }
1096
+ if (mode.type === "discount") {
1097
+ const now = Date.now();
1098
+ prunePendingDiscounts(now);
1099
+ const pendingEntries = pendingDiscounts.get(verification.address) ?? [];
1100
+ const nextEntries = pendingEntries.filter((entry) => entry.resource !== context.path);
1101
+ nextEntries.push({
1102
+ address: verification.address,
1103
+ owner: status.owner,
1104
+ generationPtr: status.generationPtr,
1105
+ resource: context.path,
1106
+ endpoint: entitlements.endpoint,
1107
+ usageContext,
1108
+ createdAt: now
1109
+ });
1110
+ pendingDiscounts.set(verification.address, nextEntries);
1111
+ return;
1112
+ }
1113
+ } catch (err) {
1114
+ await emit({
1115
+ type: "validation_failed",
1116
+ resource: context.path,
1117
+ endpoint: context.path,
1118
+ error: err instanceof Error ? err.message : "Unknown error"
1119
+ });
1120
+ if (mode.type === "linked-only") {
1121
+ return getLinkedOnlyAbort();
1122
+ }
1123
+ }
1124
+ };
1125
+ const verifyFailureHook = mode.type === "discount" ? async (context) => {
1126
+ const usageStorage = assertUsageStorage(storage, mode.type);
1127
+ const resourcePath = new URL(context.paymentPayload.resource.url).pathname;
1128
+ const payer = extractPayer(context.paymentPayload.payload);
1129
+ const now = Date.now();
1130
+ prunePendingDiscounts(now);
1131
+ const pendingEntries = payer ? pendingDiscounts.get(payer) ?? [] : [];
1132
+ const pendingIndex = pendingEntries.findIndex((entry) => entry.resource === resourcePath);
1133
+ const pending = pendingIndex >= 0 ? pendingEntries[pendingIndex] : void 0;
1134
+ if (pendingIndex >= 0) {
1135
+ pendingEntries.splice(pendingIndex, 1);
1136
+ if (pendingEntries.length > 0) {
1137
+ pendingDiscounts.set(payer, pendingEntries);
1138
+ } else {
1139
+ pendingDiscounts.delete(payer);
1140
+ }
1141
+ }
1142
+ if (!pending) return;
1143
+ if (!isUnderpaymentError(context.error)) return;
1144
+ const uses = mode.uses ?? Infinity;
1145
+ const count = await usageStorage.getUsageCount(
1146
+ pending.endpoint,
1147
+ pending.owner,
1148
+ pending.generationPtr,
1149
+ pending.usageContext
1150
+ );
1151
+ if (count >= uses) {
1152
+ await emit({
1153
+ type: "discount_exhausted",
1154
+ resource: resourcePath,
1155
+ endpoint: pending.endpoint,
1156
+ address: pending.address,
1157
+ owner: pending.owner,
1158
+ generationPtr: pending.generationPtr,
1159
+ platformAccountId: pending.usageContext.platformAccountId,
1160
+ workspaceId: pending.usageContext.workspaceId
1161
+ });
1162
+ return;
1163
+ }
1164
+ const requiredAmount = BigInt(context.requirements.amount);
1165
+ const discountedAmount = requiredAmount * BigInt(100 - mode.percent) / 100n;
1166
+ const paidAmount = extractPaidAmount(context.paymentPayload.payload);
1167
+ if (paidAmount === null || paidAmount < discountedAmount) return;
1168
+ if (paidAmount >= requiredAmount) return;
1169
+ const nextCount = count + 1;
1170
+ await usageStorage.incrementUsage(
1171
+ pending.endpoint,
1172
+ pending.owner,
1173
+ pending.generationPtr,
1174
+ pending.usageContext
1175
+ );
1176
+ await emit({
1177
+ type: "discount_applied",
1178
+ resource: resourcePath,
1179
+ endpoint: pending.endpoint,
1180
+ address: pending.address,
1181
+ owner: pending.owner,
1182
+ generationPtr: pending.generationPtr,
1183
+ platformAccountId: pending.usageContext.platformAccountId,
1184
+ workspaceId: pending.usageContext.workspaceId
1185
+ });
1186
+ await maybeEmitThresholdEvent({
1187
+ resource: resourcePath,
1188
+ endpoint: pending.endpoint,
1189
+ address: pending.address,
1190
+ owner: pending.owner,
1191
+ generationPtr: pending.generationPtr,
1192
+ mode: "discount",
1193
+ usageContext: pending.usageContext,
1194
+ count: nextCount,
1195
+ defaultThreshold: Number.isFinite(uses) ? uses : void 0
1196
+ });
1197
+ context.requirements.amount = String(paidAmount);
1198
+ return {
1199
+ recovered: true,
1200
+ result: { isValid: true, payer: pending.address }
1201
+ };
1202
+ } : void 0;
1203
+ return { requestHook, verifyFailureHook };
1204
+ }
1205
+ function assertReplayStorage(storage) {
1206
+ if (!storage) {
1207
+ throw new Error("AgentLink hooks require a storage instance with nonce replay protection");
1208
+ }
1209
+ if (typeof storage.hasUsedNonce !== "function" || typeof storage.recordNonce !== "function") {
1210
+ throw new Error("AgentLink storage must implement hasUsedNonce and recordNonce to prevent replay attacks");
1211
+ }
1212
+ }
1213
+ function assertUsageStorage(storage, modeType) {
1214
+ if (typeof storage.getUsageCount !== "function" || typeof storage.incrementUsage !== "function") {
1215
+ throw new Error(`AgentLink mode "${modeType}" requires storage methods getUsageCount and incrementUsage`);
1216
+ }
1217
+ return {
1218
+ getUsageCount: storage.getUsageCount.bind(storage),
1219
+ incrementUsage: storage.incrementUsage.bind(storage)
1220
+ };
1221
+ }
1222
+ function normalizeThresholds(thresholds) {
1223
+ return (thresholds ?? []).filter((value) => Number.isInteger(value) && value > 0);
1224
+ }
1225
+ function extractFromPayload(payload, fromAuth, fromPermit2Auth) {
1226
+ try {
1227
+ if ("authorization" in payload) {
1228
+ return fromAuth(payload.authorization);
1229
+ }
1230
+ if ("permit2Authorization" in payload) {
1231
+ return fromPermit2Auth(payload.permit2Authorization);
1232
+ }
1233
+ return null;
1234
+ } catch {
1235
+ return null;
1236
+ }
1237
+ }
1238
+ function extractPayer(payload) {
1239
+ const getFrom = (auth) => auth.from;
1240
+ return extractFromPayload(payload, getFrom, getFrom);
1241
+ }
1242
+ var UNDERPAYMENT_REASONS = [
1243
+ "invalid_exact_evm_payload_authorization_value",
1244
+ "permit2_insufficient_amount",
1245
+ "insufficient_funds"
1246
+ ];
1247
+ function isUnderpaymentError(error) {
1248
+ const reason = error.message.split(":")[0];
1249
+ return UNDERPAYMENT_REASONS.includes(reason);
1250
+ }
1251
+ function extractPaidAmount(payload) {
1252
+ return extractFromPayload(
1253
+ payload,
1254
+ (auth) => BigInt(auth.value),
1255
+ (auth) => BigInt(auth.permitted.amount)
1256
+ );
1257
+ }
1258
+
1259
+ // src/link-session.ts
1260
+ import { isAddress } from "viem";
1261
+ var InMemoryLinkSessionStore = class {
1262
+ constructor() {
1263
+ this.sessions = /* @__PURE__ */ new Map();
1264
+ }
1265
+ async set(session) {
1266
+ this.sessions.set(session.id, session);
1267
+ }
1268
+ async get(id) {
1269
+ return this.sessions.get(id) ?? null;
1270
+ }
1271
+ };
1272
+ function createSessionId() {
1273
+ if (typeof globalThis.crypto?.randomUUID === "function") {
1274
+ return `als_${globalThis.crypto.randomUUID()}`;
1275
+ }
1276
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
1277
+ const bytes = new Uint8Array(8);
1278
+ globalThis.crypto.getRandomValues(bytes);
1279
+ const randomStr = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1280
+ return `als_${Date.now().toString(36)}_${randomStr}`;
1281
+ }
1282
+ throw new Error("crypto.getRandomValues is not available in this environment");
1283
+ }
1284
+ var NETWORK_CHAIN_IDS2 = {
1285
+ base: 8453,
1286
+ "base-sepolia": 84532
1287
+ };
1288
+ function isNetworkName(value) {
1289
+ return value === "base" || value === "base-sepolia";
1290
+ }
1291
+ function parseOptionalStringRecord(input) {
1292
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
1293
+ return void 0;
1294
+ }
1295
+ const entries = Object.entries(input);
1296
+ if (entries.some(([, value]) => typeof value !== "string")) {
1297
+ return void 0;
1298
+ }
1299
+ return Object.fromEntries(entries);
1300
+ }
1301
+ function normalizeHttpsBrandingUrl(value, label) {
1302
+ if (value === void 0) {
1303
+ return { valid: true };
1304
+ }
1305
+ if (typeof value !== "string") {
1306
+ return { valid: false, error: `Hosted link sessions must use a string branding.${label}.` };
1307
+ }
1308
+ let parsed;
1309
+ try {
1310
+ parsed = new URL(value);
1311
+ } catch {
1312
+ return { valid: false, error: `Hosted link sessions must use an absolute branding.${label}.` };
1313
+ }
1314
+ if (parsed.protocol !== "https:") {
1315
+ return { valid: false, error: `Hosted link sessions only allow https branding.${label} values.` };
1316
+ }
1317
+ return { valid: true, value: parsed.toString() };
1318
+ }
1319
+ function parseBranding(input) {
1320
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
1321
+ return { valid: true };
1322
+ }
1323
+ const parsed = input;
1324
+ const branding = {};
1325
+ if (typeof parsed.platformName === "string") branding.platformName = parsed.platformName;
1326
+ if (typeof parsed.accentColor === "string") branding.accentColor = parsed.accentColor;
1327
+ const logoUrl = normalizeHttpsBrandingUrl(parsed.logoUrl, "logoUrl");
1328
+ if (!logoUrl.valid) {
1329
+ return logoUrl;
1330
+ }
1331
+ if (logoUrl.value) branding.logoUrl = logoUrl.value;
1332
+ const supportUrl = normalizeHttpsBrandingUrl(parsed.supportUrl, "supportUrl");
1333
+ if (!supportUrl.valid) {
1334
+ return supportUrl;
1335
+ }
1336
+ if (supportUrl.value) branding.supportUrl = supportUrl.value;
1337
+ return Object.keys(branding).length > 0 ? { valid: true, branding } : { valid: true };
1338
+ }
1339
+ function normalizeAllowedRedirectOrigins(allowedRedirectOrigins) {
1340
+ if (!allowedRedirectOrigins?.length) {
1341
+ return void 0;
1342
+ }
1343
+ const origins = /* @__PURE__ */ new Set();
1344
+ for (const value of allowedRedirectOrigins) {
1345
+ let parsed;
1346
+ try {
1347
+ parsed = new URL(value);
1348
+ } catch {
1349
+ throw new Error(`Invalid allowed redirect origin "${value}". Expected an absolute URL origin.`);
1350
+ }
1351
+ if (parsed.protocol !== "https:") {
1352
+ throw new Error(`Invalid allowed redirect origin "${value}". Redirect origins must use https.`);
1353
+ }
1354
+ origins.add(parsed.origin);
1355
+ }
1356
+ return origins;
1357
+ }
1358
+ function validateRedirectUrl(value, allowedOrigins) {
1359
+ if (value === void 0) {
1360
+ return { valid: true };
1361
+ }
1362
+ if (typeof value !== "string") {
1363
+ return { valid: false, error: "Hosted link sessions must use a string redirectUrl." };
1364
+ }
1365
+ let redirectUrl;
1366
+ try {
1367
+ redirectUrl = new URL(value);
1368
+ } catch {
1369
+ return { valid: false, error: "Hosted link sessions must use an absolute redirectUrl." };
1370
+ }
1371
+ if (redirectUrl.protocol !== "https:") {
1372
+ return { valid: false, error: "Hosted link sessions only allow https redirect URLs." };
1373
+ }
1374
+ if (allowedOrigins && !allowedOrigins.has(redirectUrl.origin)) {
1375
+ return {
1376
+ valid: false,
1377
+ error: `Hosted link sessions only allow redirects to these origins: ${Array.from(allowedOrigins).join(", ")}`
1378
+ };
1379
+ }
1380
+ return { valid: true, redirectUrl: redirectUrl.toString() };
1381
+ }
1382
+ function parseConsentBlob(input) {
1383
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
1384
+ return { valid: false, error: "Hosted link sessions require an embedded agent consent payload." };
1385
+ }
1386
+ const parsed = input;
1387
+ if (parsed.type !== "biomapper-agent-link") {
1388
+ return { valid: false, error: "Hosted link sessions require a biomapper-agent-link consent payload." };
1389
+ }
1390
+ if (!isNetworkName(parsed.network)) {
1391
+ return { valid: false, error: "Hosted link sessions must use a valid consent network." };
1392
+ }
1393
+ if (typeof parsed.chainId !== "number") {
1394
+ return { valid: false, error: "Hosted link sessions must include a valid consent chain id." };
1395
+ }
1396
+ if (!parsed.agent || !isAddress(parsed.agent)) {
1397
+ return { valid: false, error: "Hosted link sessions must include a valid consent agent wallet." };
1398
+ }
1399
+ if (!parsed.owner || !isAddress(parsed.owner)) {
1400
+ return { valid: false, error: "Hosted link sessions must include a valid consent owner wallet." };
1401
+ }
1402
+ if (!parsed.registry || !isAddress(parsed.registry)) {
1403
+ return { valid: false, error: "Hosted link sessions must include a valid consent registry address." };
1404
+ }
1405
+ if (!parsed.signature || !/^0x[0-9a-fA-F]+$/u.test(parsed.signature)) {
1406
+ return { valid: false, error: "Hosted link sessions must include a valid consent signature." };
1407
+ }
1408
+ if (!parsed.deadline || !/^\d+$/u.test(parsed.deadline)) {
1409
+ return { valid: false, error: "Hosted link sessions must include a valid consent expiry." };
1410
+ }
1411
+ if (!parsed.nonce || !/^\d+$/u.test(parsed.nonce)) {
1412
+ return { valid: false, error: "Hosted link sessions must include a valid consent nonce." };
1413
+ }
1414
+ if (!parsed.typedData || typeof parsed.typedData !== "object" || Array.isArray(parsed.typedData)) {
1415
+ return { valid: false, error: "Hosted link sessions must include consent typed data." };
1416
+ }
1417
+ if (!parsed.typedData.domain || typeof parsed.typedData.domain !== "object" || Array.isArray(parsed.typedData.domain) || parsed.typedData.domain.chainId !== parsed.chainId || typeof parsed.typedData.domain.verifyingContract !== "string" || parsed.typedData.domain.verifyingContract.toLowerCase() !== parsed.registry.toLowerCase()) {
1418
+ return { valid: false, error: "Hosted link sessions have mismatched consent typed data." };
1419
+ }
1420
+ if (!parsed.typedData.message || typeof parsed.typedData.message !== "object" || Array.isArray(parsed.typedData.message) || typeof parsed.typedData.message.agent !== "string" || parsed.typedData.message.agent.toLowerCase() !== parsed.agent.toLowerCase() || typeof parsed.typedData.message.owner !== "string" || parsed.typedData.message.owner.toLowerCase() !== parsed.owner.toLowerCase() || parsed.typedData.message.nonce !== parsed.nonce || parsed.typedData.message.deadline !== parsed.deadline) {
1421
+ return { valid: false, error: "Hosted link sessions have mismatched consent message details." };
1422
+ }
1423
+ return {
1424
+ valid: true,
1425
+ consent: parsed
1426
+ };
1427
+ }
1428
+ function validateLinkSession(session, options = {}) {
1429
+ if (!session || typeof session !== "object" || Array.isArray(session)) {
1430
+ return { valid: false, error: "Hosted link sessions must be objects." };
1431
+ }
1432
+ const parsed = session;
1433
+ if (!parsed.id || typeof parsed.id !== "string") {
1434
+ return { valid: false, error: "Hosted link sessions must include an id." };
1435
+ }
1436
+ if (!parsed.createdAt || typeof parsed.createdAt !== "string" || !Number.isFinite(Date.parse(parsed.createdAt))) {
1437
+ return { valid: false, error: "Hosted link sessions must include a valid createdAt timestamp." };
1438
+ }
1439
+ if (!parsed.platformAccountId || typeof parsed.platformAccountId !== "string") {
1440
+ return { valid: false, error: "Hosted link sessions must include a platformAccountId." };
1441
+ }
1442
+ if (!isNetworkName(parsed.network)) {
1443
+ return { valid: false, error: "Hosted link sessions must use a valid network." };
1444
+ }
1445
+ if (!parsed.registry || !isAddress(parsed.registry)) {
1446
+ return { valid: false, error: "Hosted link sessions must include a valid registry address." };
1447
+ }
1448
+ if (parsed.workspaceId !== void 0 && typeof parsed.workspaceId !== "string") {
1449
+ return { valid: false, error: "Hosted link sessions must use a string workspaceId when provided." };
1450
+ }
1451
+ if (parsed.expiresAt !== void 0) {
1452
+ if (typeof parsed.expiresAt !== "string" || !Number.isFinite(Date.parse(parsed.expiresAt))) {
1453
+ return { valid: false, error: "Hosted link sessions must use a valid expiresAt timestamp." };
1454
+ }
1455
+ }
1456
+ const allowedOrigins = normalizeAllowedRedirectOrigins(options.allowedRedirectOrigins);
1457
+ const redirectValidation = validateRedirectUrl(parsed.redirectUrl, allowedOrigins);
1458
+ if (!redirectValidation.valid) {
1459
+ return redirectValidation;
1460
+ }
1461
+ const consentValidation = parseConsentBlob(parsed.consent);
1462
+ if (!consentValidation.valid) {
1463
+ return consentValidation;
1464
+ }
1465
+ const consent = consentValidation.consent;
1466
+ if (consent.network !== parsed.network) {
1467
+ return { valid: false, error: "Hosted link sessions have mismatched network details." };
1468
+ }
1469
+ if (consent.chainId !== NETWORK_CHAIN_IDS2[parsed.network]) {
1470
+ return { valid: false, error: "Hosted link sessions have mismatched chain details." };
1471
+ }
1472
+ if (consent.registry.toLowerCase() !== parsed.registry.toLowerCase()) {
1473
+ return { valid: false, error: "Hosted link sessions have mismatched registry details." };
1474
+ }
1475
+ const now = options.now ?? /* @__PURE__ */ new Date();
1476
+ if (BigInt(consent.deadline) <= BigInt(Math.floor(now.getTime() / 1e3))) {
1477
+ return { valid: false, error: "Hosted link sessions have expired embedded consent." };
1478
+ }
1479
+ if (parsed.expiresAt && Date.parse(parsed.expiresAt) <= now.getTime()) {
1480
+ return { valid: false, error: "Hosted link sessions have expired." };
1481
+ }
1482
+ const branding = parseBranding(parsed.branding);
1483
+ if (!branding.valid) {
1484
+ return branding;
1485
+ }
1486
+ const metadata = parseOptionalStringRecord(parsed.metadata);
1487
+ return {
1488
+ valid: true,
1489
+ session: {
1490
+ id: parsed.id,
1491
+ createdAt: parsed.createdAt,
1492
+ platformAccountId: parsed.platformAccountId,
1493
+ ...parsed.workspaceId ? { workspaceId: parsed.workspaceId } : {},
1494
+ network: parsed.network,
1495
+ registry: parsed.registry,
1496
+ ...redirectValidation.redirectUrl ? { redirectUrl: redirectValidation.redirectUrl } : {},
1497
+ consent,
1498
+ ...branding.branding ? { branding: branding.branding } : {},
1499
+ ...metadata ? { metadata } : {},
1500
+ ...parsed.expiresAt ? { expiresAt: parsed.expiresAt } : {}
1501
+ }
1502
+ };
1503
+ }
1504
+ async function createLinkSession(input, options = {}) {
1505
+ if (!input.registry) {
1506
+ throw new Error("Hosted link sessions require a registry address.");
1507
+ }
1508
+ if (!input.consent) {
1509
+ throw new Error("Hosted link sessions require an embedded agent consent payload.");
1510
+ }
1511
+ const createdAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
1512
+ const session = {
1513
+ id: options.id ?? createSessionId(),
1514
+ createdAt,
1515
+ platformAccountId: input.platformAccountId,
1516
+ workspaceId: input.workspaceId,
1517
+ network: input.network,
1518
+ registry: input.registry,
1519
+ redirectUrl: input.redirectUrl,
1520
+ consent: input.consent,
1521
+ branding: input.branding,
1522
+ metadata: input.metadata,
1523
+ expiresAt: input.expiresAt
1524
+ };
1525
+ const validation = validateLinkSession(session, {
1526
+ now: options.now,
1527
+ allowedRedirectOrigins: options.allowedRedirectOrigins
1528
+ });
1529
+ if (!validation.valid) {
1530
+ throw new Error(validation.error);
1531
+ }
1532
+ if (options.store) {
1533
+ await options.store.set(validation.session);
1534
+ }
1535
+ await options.onEvent?.({
1536
+ type: "link.created",
1537
+ sessionId: validation.session.id,
1538
+ platformAccountId: validation.session.platformAccountId,
1539
+ workspaceId: validation.session.workspaceId,
1540
+ network: validation.session.network,
1541
+ redirectUrl: validation.session.redirectUrl,
1542
+ registry: validation.session.registry,
1543
+ platformName: validation.session.branding?.platformName
1544
+ });
1545
+ return validation.session;
1546
+ }
1547
+ async function getLinkSession(id, options) {
1548
+ return options.store.get(id);
1549
+ }
1550
+ function encodeLinkSession(session) {
1551
+ return Buffer.from(JSON.stringify(session), "utf8").toString("base64url");
1552
+ }
1553
+ function decodeLinkSession(value) {
1554
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
1555
+ }
1556
+ function buildHostedLinkUrl(baseUrl, session) {
1557
+ const url = new URL(baseUrl);
1558
+ url.searchParams.set("sessionId", session.id);
1559
+ return url.toString();
1560
+ }
1561
+ function buildEmbeddedHostedLinkUrl(baseUrl, session) {
1562
+ const url = new URL(baseUrl);
1563
+ url.searchParams.set("sessionId", session.id);
1564
+ url.searchParams.set("session", encodeLinkSession(session));
1565
+ return url.toString();
1566
+ }
1567
+
1568
+ // src/biomapper-link.ts
1569
+ import { registerExactEvmScheme } from "@x402/evm/exact/server";
1570
+ import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
1571
+ import {
1572
+ x402HTTPResourceServer
1573
+ } from "@x402/core/http";
1574
+
1575
+ // src/webhooks.ts
1576
+ var WEBHOOK_SIGNATURE_VERSION = "v1";
1577
+ var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 5 * 60;
1578
+ function createWebhookId() {
1579
+ if (typeof globalThis.crypto?.randomUUID === "function") {
1580
+ return globalThis.crypto.randomUUID();
1581
+ }
1582
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
1583
+ const bytes = new Uint8Array(8);
1584
+ globalThis.crypto.getRandomValues(bytes);
1585
+ const randomStr = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1586
+ return `evt_${Date.now().toString(36)}_${randomStr}`;
1587
+ }
1588
+ throw new Error("crypto.getRandomValues is not available in this environment");
1589
+ }
1590
+ function resolveFetch(fetchImpl) {
1591
+ if (fetchImpl) return fetchImpl;
1592
+ if (typeof fetch !== "function") {
1593
+ throw new Error("No fetch implementation is available. Pass fetchImpl to dispatch AgentLink webhooks.");
1594
+ }
1595
+ return fetch;
1596
+ }
1597
+ function createAgentLinkWebhookEnvelope(event) {
1598
+ return {
1599
+ id: createWebhookId(),
1600
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1601
+ type: event.type,
1602
+ event
1603
+ };
1604
+ }
1605
+ function stringifyWebhookEnvelope(envelope) {
1606
+ return JSON.stringify(envelope, (_key, value) => typeof value === "bigint" ? value.toString() : value);
1607
+ }
1608
+ function normalizeWebhookBody(body) {
1609
+ if (typeof body === "string") {
1610
+ return new TextEncoder().encode(body);
1611
+ }
1612
+ if (body instanceof Uint8Array) {
1613
+ return body;
1614
+ }
1615
+ if (body instanceof ArrayBuffer) {
1616
+ return new Uint8Array(body);
1617
+ }
1618
+ return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
1619
+ }
1620
+ function encodeHex(bytes) {
1621
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1622
+ }
1623
+ function constantTimeEqual(left, right) {
1624
+ if (left.length !== right.length) return false;
1625
+ let mismatch = 0;
1626
+ for (let index = 0; index < left.length; index += 1) {
1627
+ mismatch |= left.charCodeAt(index) ^ right.charCodeAt(index);
1628
+ }
1629
+ return mismatch === 0;
1630
+ }
1631
+ function parseSignatureValue(signature) {
1632
+ for (const part of signature.split(",")) {
1633
+ const trimmed = part.trim();
1634
+ if (trimmed.startsWith(`${WEBHOOK_SIGNATURE_VERSION}=`)) {
1635
+ return trimmed.slice(WEBHOOK_SIGNATURE_VERSION.length + 1);
1636
+ }
1637
+ }
1638
+ return null;
1639
+ }
1640
+ async function createHmacSignature(secret, timestamp, body) {
1641
+ if (!globalThis.crypto?.subtle) {
1642
+ throw new Error("Web Crypto is required to sign AgentLink webhooks in this runtime.");
1643
+ }
1644
+ const encoder = new TextEncoder();
1645
+ const key = await globalThis.crypto.subtle.importKey(
1646
+ "raw",
1647
+ encoder.encode(secret),
1648
+ { name: "HMAC", hash: "SHA-256" },
1649
+ false,
1650
+ ["sign"]
1651
+ );
1652
+ const payload = new Uint8Array([...encoder.encode(`${timestamp}.`), ...normalizeWebhookBody(body)]);
1653
+ const signature = await globalThis.crypto.subtle.sign("HMAC", key, payload);
1654
+ return `${WEBHOOK_SIGNATURE_VERSION}=${encodeHex(new Uint8Array(signature))}`;
1655
+ }
1656
+ async function verifyAgentLinkWebhookSignature(options) {
1657
+ if (!/^\d+$/u.test(options.timestamp)) {
1658
+ return { valid: false, error: "Invalid webhook timestamp." };
1659
+ }
1660
+ const timestampSeconds = Number(options.timestamp);
1661
+ const nowSeconds = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
1662
+ const toleranceSeconds = options.toleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
1663
+ if (Math.abs(nowSeconds - timestampSeconds) > toleranceSeconds) {
1664
+ return { valid: false, error: "Webhook timestamp is outside the allowed tolerance." };
1665
+ }
1666
+ const providedSignature = parseSignatureValue(options.signature);
1667
+ if (!providedSignature) {
1668
+ return { valid: false, error: "Invalid webhook signature format." };
1669
+ }
1670
+ const expectedSignature = parseSignatureValue(
1671
+ await createHmacSignature(options.secret, options.timestamp, options.body)
1672
+ );
1673
+ if (!expectedSignature || !constantTimeEqual(providedSignature, expectedSignature)) {
1674
+ return { valid: false, error: "Webhook signature mismatch." };
1675
+ }
1676
+ return { valid: true };
1677
+ }
1678
+ async function dispatchAgentLinkWebhook(event, options) {
1679
+ const fetchImpl = resolveFetch(options.fetchImpl);
1680
+ const envelope = createAgentLinkWebhookEnvelope(event);
1681
+ const urls = Array.isArray(options.url) ? options.url : [options.url];
1682
+ const body = stringifyWebhookEnvelope(envelope);
1683
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
1684
+ const signature = options.secret ? await createHmacSignature(options.secret, timestamp, body) : void 0;
1685
+ for (const url of urls) {
1686
+ const includeLegacySecretHeader = options.includeLegacySecretHeader ?? true;
1687
+ const response = await fetchImpl(url, {
1688
+ method: "POST",
1689
+ headers: {
1690
+ "content-type": "application/json",
1691
+ ...options.secret ? {
1692
+ "x-agentlink-webhook-timestamp": timestamp,
1693
+ "x-agentlink-webhook-signature": signature,
1694
+ ...includeLegacySecretHeader ? { "x-agentlink-webhook-secret": options.secret } : {}
1695
+ } : {},
1696
+ ...options.headers
1697
+ },
1698
+ body
1699
+ });
1700
+ if (!response.ok) {
1701
+ throw new Error(`AgentLink webhook delivery failed for ${url} with status ${response.status}`);
1702
+ }
1703
+ }
1704
+ return envelope;
1705
+ }
1706
+ function createAgentLinkWebhookDispatcher(options) {
1707
+ return async (event) => {
1708
+ await dispatchAgentLinkWebhook(event, options);
1709
+ };
1710
+ }
1711
+
1712
+ // src/hono.ts
1713
+ var HonoAdapter = class {
1714
+ constructor(context) {
1715
+ this.context = context;
1716
+ }
1717
+ getHeader(name) {
1718
+ return this.context.req.header(name);
1719
+ }
1720
+ getMethod() {
1721
+ return this.context.req.method;
1722
+ }
1723
+ getPath() {
1724
+ return this.context.req.path;
1725
+ }
1726
+ getUrl() {
1727
+ return this.context.req.url;
1728
+ }
1729
+ getAcceptHeader() {
1730
+ return this.context.req.header("Accept") ?? "";
1731
+ }
1732
+ getUserAgent() {
1733
+ return this.context.req.header("User-Agent") ?? "";
1734
+ }
1735
+ getQueryParams() {
1736
+ return this.context.req.query();
1737
+ }
1738
+ getQueryParam(name) {
1739
+ return this.context.req.query(name);
1740
+ }
1741
+ async getBody() {
1742
+ return await this.context.req.json();
1743
+ }
1744
+ };
1745
+ function createHonoPaymentMiddlewareFromHTTPServer(httpServer, paywallConfig, paywall) {
1746
+ if (paywall) {
1747
+ httpServer.registerPaywallProvider(paywall);
1748
+ }
1749
+ let initialized = false;
1750
+ let initializePromise = null;
1751
+ async function initialize() {
1752
+ if (initialized) return;
1753
+ if (!initializePromise) {
1754
+ initializePromise = httpServer.initialize().then(() => {
1755
+ initialized = true;
1756
+ }).catch((error) => {
1757
+ initializePromise = null;
1758
+ throw error;
1759
+ });
1760
+ }
1761
+ await initializePromise;
1762
+ }
1763
+ const middleware = async (context, next) => {
1764
+ const typedContext = context;
1765
+ const adapter = new HonoAdapter(typedContext);
1766
+ const requestContext = {
1767
+ adapter,
1768
+ path: typedContext.req.path,
1769
+ method: typedContext.req.method,
1770
+ paymentHeader: adapter.getHeader("payment-signature") || adapter.getHeader("x-payment")
1771
+ };
1772
+ if (!httpServer.requiresPayment(requestContext)) {
1773
+ return next();
1774
+ }
1775
+ await initialize();
1776
+ const result = await httpServer.processHTTPRequest(requestContext, paywallConfig);
1777
+ switch (result.type) {
1778
+ case "no-payment-required":
1779
+ return next();
1780
+ case "payment-error": {
1781
+ const { response } = result;
1782
+ for (const [key, value] of Object.entries(response.headers)) {
1783
+ typedContext.header(key, value);
1784
+ }
1785
+ if (response.isHtml) {
1786
+ return typedContext.html(String(response.body ?? ""), response.status);
1787
+ }
1788
+ return typedContext.json(response.body ?? {}, response.status);
1789
+ }
1790
+ case "payment-verified": {
1791
+ const { paymentPayload, paymentRequirements, declaredExtensions } = result;
1792
+ await next();
1793
+ let response = typedContext.res;
1794
+ if (!response || response.status >= 400) {
1795
+ return;
1796
+ }
1797
+ const responseBody = Buffer.from(await response.clone().arrayBuffer());
1798
+ typedContext.res = void 0;
1799
+ const settlement = await httpServer.processSettlement(
1800
+ paymentPayload,
1801
+ paymentRequirements,
1802
+ declaredExtensions,
1803
+ {
1804
+ request: requestContext,
1805
+ responseBody
1806
+ }
1807
+ );
1808
+ if (!settlement.success) {
1809
+ const { response: errorResponse } = settlement;
1810
+ const body = errorResponse.isHtml ? String(errorResponse.body ?? "") : JSON.stringify(errorResponse.body ?? {});
1811
+ response = new Response(body, {
1812
+ status: errorResponse.status,
1813
+ headers: errorResponse.headers
1814
+ });
1815
+ } else {
1816
+ for (const [key, value] of Object.entries(settlement.headers)) {
1817
+ response.headers.set(key, value);
1818
+ }
1819
+ }
1820
+ typedContext.res = response;
1821
+ return;
1822
+ }
1823
+ }
1824
+ };
1825
+ return { middleware, initialize };
1826
+ }
1827
+
1828
+ // src/biomapper-link.ts
1829
+ var NETWORK_TO_CHAIN_ID = {
1830
+ base: "eip155:8453",
1831
+ "base-sepolia": "eip155:84532"
1832
+ };
1833
+ var DEFAULT_X402_FACILITATOR_URL = "https://x402.org/facilitator";
1834
+ var hasWarnedAboutInMemoryStorageDefault = false;
1835
+ function createBiomapperLink(options) {
1836
+ const chainId = NETWORK_TO_CHAIN_ID[options.network];
1837
+ const storage = options.storage ?? resolveDefaultStorage();
1838
+ const registry = createBiomapperRegistryVerifier({
1839
+ contractAddress: options.registry,
1840
+ network: options.network,
1841
+ rpcUrl: options.rpcUrl
1842
+ });
1843
+ let onEvent = options.onEvent;
1844
+ if (options.webhook) {
1845
+ const webhookDispatcher = createAgentLinkWebhookDispatcher({
1846
+ url: options.webhook.url,
1847
+ secret: options.webhook.secret,
1848
+ headers: options.webhook.headers
1849
+ });
1850
+ const userOnEvent = onEvent;
1851
+ onEvent = async (event) => {
1852
+ await webhookDispatcher(event);
1853
+ await userOnEvent?.(event);
1854
+ };
1855
+ }
1856
+ const hooks = createAgentLinkHooks({
1857
+ registry,
1858
+ storage,
1859
+ mode: options.mode,
1860
+ rpcUrl: options.rpcUrl,
1861
+ onEvent,
1862
+ entitlements: options.entitlements,
1863
+ usageThresholds: options.usageThresholds
1864
+ });
1865
+ const declare = (overrides) => declareAgentLinkExtension({
1866
+ network: chainId,
1867
+ mode: options.mode,
1868
+ statement: options.statement,
1869
+ domain: options.domain,
1870
+ ...overrides
1871
+ });
1872
+ let resourceServer;
1873
+ let httpServer;
1874
+ let middleware = async () => {
1875
+ throw new Error(
1876
+ "createBiomapperLink was used as middleware without x402 route configuration. Pass `protect` or `routes` to enable the one-call middleware path."
1877
+ );
1878
+ };
1879
+ let initialize = async () => {
1880
+ };
1881
+ const routes = buildRoutes(options, declare(), chainId);
1882
+ if (routes) {
1883
+ resourceServer = new x402ResourceServer(resolveFacilitatorClients(options));
1884
+ resourceServer.registerExtension(agentlinkResourceServerExtension);
1885
+ if (hooks.verifyFailureHook) {
1886
+ resourceServer.onVerifyFailure(async (context) => {
1887
+ const url = context.paymentPayload.resource?.url;
1888
+ if (!url) return;
1889
+ return hooks.verifyFailureHook?.({
1890
+ paymentPayload: {
1891
+ resource: { url },
1892
+ payload: context.paymentPayload.payload
1893
+ },
1894
+ requirements: {
1895
+ amount: context.requirements.amount
1896
+ },
1897
+ error: context.error
1898
+ });
1899
+ });
1900
+ }
1901
+ if (options.schemes?.length) {
1902
+ for (const scheme of options.schemes) {
1903
+ resourceServer.register(scheme.network, scheme.server);
1904
+ }
1905
+ } else {
1906
+ registerExactEvmScheme(resourceServer, {
1907
+ networks: collectExactEvmNetworks(routes)
1908
+ });
1909
+ }
1910
+ httpServer = new x402HTTPResourceServer(resourceServer, routes).onProtectedRequest(hooks.requestHook);
1911
+ const hono = createHonoPaymentMiddlewareFromHTTPServer(httpServer, options.paywallConfig, options.paywall);
1912
+ middleware = hono.middleware;
1913
+ initialize = hono.initialize;
1914
+ }
1915
+ return Object.assign(middleware, {
1916
+ extension: agentlinkResourceServerExtension,
1917
+ declare,
1918
+ requestHook: hooks.requestHook,
1919
+ verifyFailureHook: hooks.verifyFailureHook,
1920
+ middleware,
1921
+ initialize,
1922
+ resourceServer,
1923
+ httpServer
1924
+ });
1925
+ }
1926
+ function resolveDefaultStorage() {
1927
+ if (process.env.NODE_ENV === "production") {
1928
+ throw new Error(
1929
+ "createBiomapperLink requires an explicit storage backend in production. Pass persistent/shared storage instead of relying on the in-memory development default."
1930
+ );
1931
+ }
1932
+ if (!hasWarnedAboutInMemoryStorageDefault) {
1933
+ hasWarnedAboutInMemoryStorageDefault = true;
1934
+ console.warn(
1935
+ "[agentlink] createBiomapperLink is using InMemoryAgentLinkStorage because no storage backend was provided. This is only safe for local development and tests."
1936
+ );
1937
+ }
1938
+ return new InMemoryAgentLinkStorage();
1939
+ }
1940
+ function buildRoutes(options, agentlinkExtensions, defaultNetwork) {
1941
+ if (options.routes && options.protect) {
1942
+ throw new Error("Pass either `routes` or `protect` to createBiomapperLink, not both.");
1943
+ }
1944
+ if (options.routes) {
1945
+ return mergeAgentLinkExtensions(options.routes, agentlinkExtensions);
1946
+ }
1947
+ if (!options.protect) return void 0;
1948
+ return Object.fromEntries(
1949
+ Object.entries(options.protect).map(([pattern, config]) => [
1950
+ pattern,
1951
+ {
1952
+ resource: config.resource,
1953
+ description: config.description,
1954
+ mimeType: config.mimeType,
1955
+ customPaywallHtml: config.customPaywallHtml,
1956
+ unpaidResponseBody: config.unpaidResponseBody,
1957
+ settlementFailedResponseBody: config.settlementFailedResponseBody,
1958
+ extensions: {
1959
+ ...agentlinkExtensions,
1960
+ ...config.extensions ?? {}
1961
+ },
1962
+ accepts: {
1963
+ scheme: config.scheme ?? "exact",
1964
+ network: config.network ?? defaultNetwork,
1965
+ payTo: config.payTo,
1966
+ price: config.price,
1967
+ maxTimeoutSeconds: config.maxTimeoutSeconds,
1968
+ extra: config.extra
1969
+ }
1970
+ }
1971
+ ])
1972
+ );
1973
+ }
1974
+ function mergeAgentLinkExtensions(routes, agentlinkExtensions) {
1975
+ if (isRouteConfig(routes)) {
1976
+ return {
1977
+ ...routes,
1978
+ extensions: {
1979
+ ...agentlinkExtensions,
1980
+ ...routes.extensions ?? {}
1981
+ }
1982
+ };
1983
+ }
1984
+ return Object.fromEntries(
1985
+ Object.entries(routes).map(([pattern, route]) => [
1986
+ pattern,
1987
+ {
1988
+ ...route,
1989
+ extensions: {
1990
+ ...agentlinkExtensions,
1991
+ ...route.extensions ?? {}
1992
+ }
1993
+ }
1994
+ ])
1995
+ );
1996
+ }
1997
+ function collectExactEvmNetworks(routes) {
1998
+ const routeList = isRouteConfig(routes) ? [routes] : Object.values(routes);
1999
+ const networks = /* @__PURE__ */ new Set();
2000
+ for (const route of routeList) {
2001
+ const accepts = normalizePaymentOptions(route);
2002
+ for (const option of accepts) {
2003
+ if (option.scheme !== "exact") {
2004
+ throw new Error(
2005
+ `Auto-registration only supports the x402 "exact" scheme. Register custom schemes via \`schemes\` for "${option.scheme}".`
2006
+ );
2007
+ }
2008
+ if (!option.network.startsWith("eip155:")) {
2009
+ throw new Error(
2010
+ `Auto-registration only supports EVM payment networks. Register custom schemes via \`schemes\` for "${option.network}".`
2011
+ );
2012
+ }
2013
+ networks.add(option.network);
2014
+ }
2015
+ }
2016
+ return [...networks];
2017
+ }
2018
+ function isRouteConfig(routes) {
2019
+ return "accepts" in routes;
2020
+ }
2021
+ function normalizePaymentOptions(route) {
2022
+ return Array.isArray(route.accepts) ? route.accepts : [route.accepts];
2023
+ }
2024
+ function resolveFacilitatorClients(options) {
2025
+ return options.facilitator ?? new HTTPFacilitatorClient({ url: options.facilitatorUrl ?? DEFAULT_X402_FACILITATOR_URL });
2026
+ }
2027
+
2028
+ // src/next.ts
2029
+ var NextAdapter = class {
2030
+ constructor(request) {
2031
+ this.request = request;
2032
+ }
2033
+ getHeader(name) {
2034
+ return this.request.headers.get(name) ?? void 0;
2035
+ }
2036
+ getMethod() {
2037
+ return this.request.method;
2038
+ }
2039
+ getPath() {
2040
+ return new URL(this.request.url).pathname;
2041
+ }
2042
+ getUrl() {
2043
+ return this.request.url;
2044
+ }
2045
+ getAcceptHeader() {
2046
+ return this.request.headers.get("Accept") ?? "";
2047
+ }
2048
+ getUserAgent() {
2049
+ return this.request.headers.get("User-Agent") ?? "";
2050
+ }
2051
+ getQueryParams() {
2052
+ const params = {};
2053
+ for (const [key, value] of new URL(this.request.url).searchParams.entries()) {
2054
+ const existing = params[key];
2055
+ if (existing === void 0) {
2056
+ params[key] = value;
2057
+ } else if (Array.isArray(existing)) {
2058
+ existing.push(value);
2059
+ } else {
2060
+ params[key] = [existing, value];
2061
+ }
2062
+ }
2063
+ return params;
2064
+ }
2065
+ getQueryParam(name) {
2066
+ const values = new URL(this.request.url).searchParams.getAll(name);
2067
+ if (values.length === 0) return void 0;
2068
+ return values.length === 1 ? values[0] : values;
2069
+ }
2070
+ async getBody() {
2071
+ try {
2072
+ return await this.request.clone().json();
2073
+ } catch {
2074
+ return void 0;
2075
+ }
2076
+ }
2077
+ };
2078
+ function createNextPaymentHandlerFromHTTPServer(httpServer, paywallConfig, paywall) {
2079
+ if (paywall) {
2080
+ httpServer.registerPaywallProvider(paywall);
2081
+ }
2082
+ let initialized = false;
2083
+ let initializePromise = null;
2084
+ async function initialize() {
2085
+ if (initialized) return;
2086
+ if (!initializePromise) {
2087
+ initializePromise = httpServer.initialize().then(() => {
2088
+ initialized = true;
2089
+ }).catch((error) => {
2090
+ initializePromise = null;
2091
+ throw error;
2092
+ });
2093
+ }
2094
+ await initializePromise;
2095
+ }
2096
+ function wrap(handler) {
2097
+ return async (request) => {
2098
+ const adapter = new NextAdapter(request);
2099
+ const requestContext = {
2100
+ adapter,
2101
+ path: new URL(request.url).pathname,
2102
+ method: request.method,
2103
+ paymentHeader: adapter.getHeader("payment-signature") || adapter.getHeader("x-payment")
2104
+ };
2105
+ if (!httpServer.requiresPayment(requestContext)) {
2106
+ return handler(request);
2107
+ }
2108
+ await initialize();
2109
+ const result = await httpServer.processHTTPRequest(requestContext, paywallConfig);
2110
+ switch (result.type) {
2111
+ case "no-payment-required":
2112
+ return handler(request);
2113
+ case "payment-error": {
2114
+ const { response } = result;
2115
+ const body = response.isHtml ? String(response.body ?? "") : JSON.stringify(response.body ?? {});
2116
+ return new Response(body, {
2117
+ status: response.status,
2118
+ headers: response.headers
2119
+ });
2120
+ }
2121
+ case "payment-verified": {
2122
+ const { paymentPayload, paymentRequirements, declaredExtensions } = result;
2123
+ const handlerResponse = await handler(request);
2124
+ if (handlerResponse.status >= 400) {
2125
+ return handlerResponse;
2126
+ }
2127
+ const responseBody = Buffer.from(await handlerResponse.clone().arrayBuffer());
2128
+ const settlement = await httpServer.processSettlement(
2129
+ paymentPayload,
2130
+ paymentRequirements,
2131
+ declaredExtensions,
2132
+ { request: requestContext, responseBody }
2133
+ );
2134
+ if (!settlement.success) {
2135
+ const { response: errorResponse } = settlement;
2136
+ const body = errorResponse.isHtml ? String(errorResponse.body ?? "") : JSON.stringify(errorResponse.body ?? {});
2137
+ return new Response(body, {
2138
+ status: errorResponse.status,
2139
+ headers: errorResponse.headers
2140
+ });
2141
+ }
2142
+ const headers = new Headers(handlerResponse.headers);
2143
+ for (const [key, value] of Object.entries(settlement.headers)) {
2144
+ headers.set(key, value);
2145
+ }
2146
+ return new Response(handlerResponse.body, {
2147
+ status: handlerResponse.status,
2148
+ headers
2149
+ });
2150
+ }
2151
+ }
2152
+ };
2153
+ }
2154
+ return { wrap, initialize };
2155
+ }
2156
+ export {
2157
+ AGENTLINK,
2158
+ AGENT_LINK_TYPES,
2159
+ AgentLinkPayloadSchema,
2160
+ BIOMAPPER_AGENT_REGISTRY_ABI,
2161
+ BIOMAPPER_AGENT_REGISTRY_NAME,
2162
+ BIOMAPPER_AGENT_REGISTRY_VERSION,
2163
+ BIOMAPPER_APP_URLS,
2164
+ BRIDGED_BIOMAPPER_ADDRESSES,
2165
+ BRIDGED_BIOMAPPER_READ_ABI,
2166
+ BiomapperNetworkSchema,
2167
+ BiomapperQueryError,
2168
+ CHECK_AGENT_STATUS_TOOL_DESCRIPTION,
2169
+ CHECK_AGENT_STATUS_TOOL_NAME,
2170
+ CheckAgentStatusInputSchema,
2171
+ CheckAgentStatusResultSchema,
2172
+ GET_BIOMAPPER_INFO_TOOL_DESCRIPTION,
2173
+ GET_BIOMAPPER_INFO_TOOL_NAME,
2174
+ GET_CURRENT_GENERATION_TOOL_DESCRIPTION,
2175
+ GET_CURRENT_GENERATION_TOOL_NAME,
2176
+ GetBiomapperInfoInputSchema,
2177
+ GetBiomapperInfoResultSchema,
2178
+ GetCurrentGenerationInputSchema,
2179
+ GetCurrentGenerationResultSchema,
2180
+ InMemoryAgentLinkStorage,
2181
+ InMemoryLinkSessionStore,
2182
+ agentlinkResourceServerExtension,
2183
+ buildAgentLinkSchema,
2184
+ buildAgentLinkTypedData,
2185
+ buildAgentLinkUsageKey,
2186
+ buildEmbeddedHostedLinkUrl,
2187
+ buildHostedLinkUrl,
2188
+ createAgentLinkConsent,
2189
+ createAgentLinkHooks,
2190
+ createAgentLinkWebhookDispatcher,
2191
+ createAgentLinkWebhookEnvelope,
2192
+ createBiomapperLink,
2193
+ createBiomapperQueryClient,
2194
+ createBiomapperRegistryVerifier,
2195
+ createHonoPaymentMiddlewareFromHTTPServer,
2196
+ createLinkSession,
2197
+ createNextPaymentHandlerFromHTTPServer,
2198
+ declareAgentLinkExtension,
2199
+ decodeLinkSession,
2200
+ dispatchAgentLinkWebhook,
2201
+ encodeLinkSession,
2202
+ extractEVMChainId,
2203
+ formatSIWEMessage,
2204
+ getLinkSession,
2205
+ parseAgentLinkHeader,
2206
+ validateAgentLinkMessage,
2207
+ validateLinkSession,
2208
+ verifyAgentLinkSignature,
2209
+ verifyAgentLinkWebhookSignature,
2210
+ verifyEVMSignature
2211
+ };
2212
+ //# sourceMappingURL=index.mjs.map