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