@muretai/agent-entry 1.8.0 → 1.9.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/README.md +35 -13
- package/examples/server.mjs +8 -1
- package/muretai-agent-entry.mjs +100 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ answers questions and hands off nothing, is a signed claim you cannot keep.
|
|
|
114
114
|
| `domains` | none | the domains this entry speaks for (see below) |
|
|
115
115
|
| `basePath` | from `baseUrl` | the path this entry answers at, derived rather than set beside it |
|
|
116
116
|
| `wbaVerifiers` | none | a JWKS document (`{"keys":[…]}`) of Ed25519 keys whose holders this entry should **recognise** on inbound signed requests (Web Bot Auth / RFC 9421 — see *Who is knocking*). Recognition only adds `env.wba_did` and a visit count; it never changes a verdict |
|
|
117
|
-
| `observer` | none | called once per message with the same envelope your responder gets, plus `stage`, `identified` and `
|
|
117
|
+
| `observer` | none | called once per message with the same envelope your responder gets, plus `stage`, `identified`, `ua_family` and `client_class`, **after** the verdict — for counting, logging, analytics. It cannot matter: its return is discarded, a throw is swallowed, a promise is never awaited, so a slow or broken watcher cannot delay or change one byte of the signed reply. See [Counting visits](#counting-visits-without-handing-over-your-customer-list) |
|
|
118
118
|
| `howToUrl` | none | a page a keyless visitor is pointed at as a worked example. **Empty means omitted** — the refusal already teaches the whole recipe without it, and a reference implementation must not stamp somebody else's docs host into every door built from it. Only set it to a URL you operate, and only after checking it resolves |
|
|
119
119
|
| `name`, `description`, `version` | — | the card's own words. `description` is the line a person reads in a directory listing — and the right place to say what you record about visitors, since it is fetched **before** the knock |
|
|
120
120
|
|
|
@@ -131,12 +131,21 @@ door counts it:
|
|
|
131
131
|
```js
|
|
132
132
|
entry.stats()
|
|
133
133
|
// { gptbot: { card_get: 12, signed_post: 3 },
|
|
134
|
-
// browser: { notice_get: 5 } }
|
|
134
|
+
// browser: { notice_get: 5, card_get: 2 } }
|
|
135
|
+
|
|
136
|
+
entry.clientStats()
|
|
137
|
+
// { 'declared-agent': { card_get: 12, signed_post: 3 },
|
|
138
|
+
// 'human-like': { notice_get: 5 },
|
|
139
|
+
// 'stealth-agent': { card_get: 2 } }
|
|
135
140
|
```
|
|
136
141
|
|
|
137
142
|
Each request's `User-Agent` is classified into a fixed family (`claude-user`,
|
|
138
143
|
`claudebot`, `gptbot`, `openai`, `perplexity`, `google-extended`, `muretai-node`,
|
|
139
|
-
`
|
|
144
|
+
`camoufox`, `playwright`, `puppeteer`, `selenium`, `headless-chrome`,
|
|
145
|
+
`curl`, `browser`, `none`/`other`) and counted by stage. `clientStats()` then
|
|
146
|
+
folds those families into four owner-facing classes — `declared-agent`,
|
|
147
|
+
`named-tool`, `stealth-agent`, `human-like` — so a Firefox-looking fetch of the
|
|
148
|
+
card is counted as a stealth agent, not as a person. In-process state like the
|
|
140
149
|
ledger — read it, log it, ship it to your analytics; it is never served on the wire.
|
|
141
150
|
Every caller also gets one nudge: `GET /` answers with a single `Link:` field carrying
|
|
142
151
|
two relations — `rel="service-desc"` (RFC 8631) first, then the door pointer
|
|
@@ -154,6 +163,15 @@ five stages now carry it, refusals included, so "which clients got in and which
|
|
|
154
163
|
is one query instead of two half-answers. Nothing else moved: no wire byte, no verdict, no ledger
|
|
155
164
|
row, no rate lane, and `stats()` is unchanged.
|
|
156
165
|
|
|
166
|
+
**Since 1.9.0 a Firefox-looking card fetch is not a human.** `stats()` still files it under
|
|
167
|
+
`browser` — Camoufox's published shape is a clean Firefox UA, and that is the point of
|
|
168
|
+
stealth. `clientStats()` splits it: a browser that only opened the notice is `human-like`;
|
|
169
|
+
the same UA on the card or the door is `stealth-agent`; a leaking automation token
|
|
170
|
+
(`playwright`, `camoufox`, …) is `named-tool`. Export `bodySignpost()` and put that
|
|
171
|
+
`<a>` in the page body, because a snapshot client never sees the header or the
|
|
172
|
+
`<head>` tag. Still observation only — the same POST with or without that UA is the
|
|
173
|
+
same refusal.
|
|
174
|
+
|
|
157
175
|
One rule holds this together, enforced by the contract suite rather than promised:
|
|
158
176
|
**a User-Agent never affects `verified`, an account row, a rate limit, or any
|
|
159
177
|
refusal.** A UA string is written by the client; a door that trusted it would be a
|
|
@@ -228,8 +246,8 @@ framework, an edge worker — that notice never renders**, and your home page is
|
|
|
228
246
|
people with nothing machine-readable in it. The address ends up published in a card nobody was
|
|
229
247
|
told to fetch.
|
|
230
248
|
|
|
231
|
-
So put the pointer on every page a visitor might land on, in **
|
|
232
|
-
fallback for the
|
|
249
|
+
So put the pointer on every page a visitor might land on, in **all three** spellings. None is a
|
|
250
|
+
fallback for the others:
|
|
233
251
|
|
|
234
252
|
```
|
|
235
253
|
Link: </.well-known/agent-card.json>; rel="https://muretai.net/rel/agent-entry"
|
|
@@ -237,13 +255,17 @@ Link: </.well-known/agent-card.json>; rel="https://muretai.net/rel/agent-entry"
|
|
|
237
255
|
|
|
238
256
|
```html
|
|
239
257
|
<link rel="https://muretai.net/rel/agent-entry" href="/.well-known/agent-card.json">
|
|
258
|
+
<a href="/.well-known/agent-card.json" rel="https://muretai.net/rel/agent-entry">This site answers agents at /.well-known/agent-card.json</a>
|
|
240
259
|
```
|
|
241
260
|
|
|
242
261
|
The relation is an opaque **identifier**, matched as a string — nothing about resolving an agent
|
|
243
|
-
endpoint requires a request to that host. The
|
|
262
|
+
endpoint requires a request to that host. The three spellings exist because three kinds of client
|
|
244
263
|
have opposite blind spots: an agent that fetches with a plain `curl` (no `-i`) never sees the
|
|
245
|
-
header
|
|
246
|
-
|
|
264
|
+
header; one that reads only headers never parses the HTML; and a snapshot / ARIA client
|
|
265
|
+
(Camofox, Playwright accessibility dumps) sees only `<body>`, so the `<link>` in `<head>`
|
|
266
|
+
vanishes too. Shipping one is a coin flip on which kind arrived. The module exports
|
|
267
|
+
`bodySignpost()` so the body `<a>` is one function call, not a string you have to keep in
|
|
268
|
+
sync with the relation URI.
|
|
247
269
|
|
|
248
270
|
We know because we shipped one. An agent that had never been told about our door was handed only
|
|
249
271
|
the domain, fetched the page, read the copy written for humans, and stopped — while the door had
|
|
@@ -253,13 +275,13 @@ Then check it from outside, because this is exactly the class of thing that look
|
|
|
253
275
|
|
|
254
276
|
```bash
|
|
255
277
|
curl -sI https://studio.example/ | grep -i '^link:' # the header half
|
|
256
|
-
curl -s https://studio.example/ | grep 'rel/agent-entry' # the tag half
|
|
278
|
+
curl -s https://studio.example/ | grep 'rel/agent-entry' # the tag half AND the body <a>
|
|
257
279
|
```
|
|
258
280
|
|
|
259
|
-
Worth knowing before you call it done: **
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
281
|
+
Worth knowing before you call it done: **the header and the `<head>` tag disappear in a fetch
|
|
282
|
+
that converts the page to markdown, and in an accessibility snapshot that only sees `<body>`**
|
|
283
|
+
— a common way an agent browser reads the web. The body `<a>` is the spelling those clients
|
|
284
|
+
can still see.
|
|
263
285
|
|
|
264
286
|
### Check that your own CDN is not refusing your door
|
|
265
287
|
|
package/examples/server.mjs
CHANGED
|
@@ -183,17 +183,24 @@ const server = entry.listen(port, host, () => {
|
|
|
183
183
|
// printed only when they changed. `[ua]` is greppable; the shape is entry.stats()
|
|
184
184
|
// verbatim ({family: {stage: n}}). `unref()` so the timer never holds the process open.
|
|
185
185
|
let lastStats = '';
|
|
186
|
+
let lastClient = '';
|
|
186
187
|
setInterval(() => {
|
|
187
188
|
// Sorted keys at every level so the line is stable run to run (and diffable against
|
|
188
189
|
// the Python runner's `json.dumps(..., sort_keys=True)` spelling of the same shape).
|
|
189
|
-
const
|
|
190
|
+
const stable = (obj) => JSON.stringify(obj, (k, v) =>
|
|
190
191
|
(v && typeof v === 'object' && !Array.isArray(v))
|
|
191
192
|
? Object.fromEntries(Object.keys(v).sort().map((key) => [key, v[key]]))
|
|
192
193
|
: v);
|
|
194
|
+
const line = stable(entry.stats());
|
|
193
195
|
if (line !== '{}' && line !== lastStats) {
|
|
194
196
|
console.log(`[ua] ${line}`);
|
|
195
197
|
lastStats = line;
|
|
196
198
|
}
|
|
199
|
+
const clientLine = stable(entry.clientStats());
|
|
200
|
+
if (clientLine !== '{}' && clientLine !== lastClient) {
|
|
201
|
+
console.log(`[client] ${clientLine}`);
|
|
202
|
+
lastClient = clientLine;
|
|
203
|
+
}
|
|
197
204
|
}, 60_000).unref();
|
|
198
205
|
|
|
199
206
|
// A port collision is the first thing anyone running this twice hits (a previous run that was
|
package/muretai-agent-entry.mjs
CHANGED
|
@@ -107,6 +107,17 @@ export const SIGNED_ENVELOPE_SCHEME = 'did-key-ed25519';
|
|
|
107
107
|
* examples/agent_entry_reference.py. */
|
|
108
108
|
export const AGENT_ENTRY_REL = 'https://muretai.net/rel/agent-entry';
|
|
109
109
|
|
|
110
|
+
/** The body `<a>` a site puts on its own front page so a snapshot / ARIA / `a[href]`
|
|
111
|
+
* client (Camofox, Playwright accessibility dumps, markdown converters) can still find
|
|
112
|
+
* the door. `<link rel>` in `<head>` and the HTTP `Link` header both vanish in those
|
|
113
|
+
* views; an in-body anchor with this relation does not. `href` is the card path this
|
|
114
|
+
* entry actually answers (mount-prefixed when the entry sits under a path). Must match
|
|
115
|
+
* `body_signpost` in examples/agent_entry_reference.py. */
|
|
116
|
+
export function bodySignpost(href = AGENT_CARD_PATH) {
|
|
117
|
+
const path = typeof href === 'string' && href ? href : AGENT_CARD_PATH;
|
|
118
|
+
return `<a href="${path}" rel="${AGENT_ENTRY_REL}">This site answers agents at ${path}</a>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
/** Where a keyless visitor is sent to learn how to mint an identity and sign. It rides in
|
|
111
122
|
* the card AND in the refusal, so an agent that has only one of the two still has the URL.
|
|
112
123
|
*
|
|
@@ -332,6 +343,16 @@ export const UA_FAMILIES = [
|
|
|
332
343
|
['perplexity', 'perplexity'],
|
|
333
344
|
['google-extended', 'google-extended'],
|
|
334
345
|
['muretai-node', 'muretai-node'],
|
|
346
|
+
// Leaking automation UAs. Must sit BEFORE `mozilla`: Camoufox/Playwright Firefox UAs
|
|
347
|
+
// start with "Mozilla/5.0 …" and a needle after that would never fire. A patched
|
|
348
|
+
// Camoufox sends a clean Firefox UA and classifies as `browser` — that is the
|
|
349
|
+
// stealth-agent case `clientClass` exists for, not a miss in this table.
|
|
350
|
+
['camoufox', 'camoufox'],
|
|
351
|
+
['camofox', 'camoufox'],
|
|
352
|
+
['playwright', 'playwright'],
|
|
353
|
+
['puppeteer', 'puppeteer'],
|
|
354
|
+
['selenium', 'selenium'],
|
|
355
|
+
['headlesschrome', 'headless-chrome'],
|
|
335
356
|
['curl', 'curl'],
|
|
336
357
|
['mozilla', 'browser'],
|
|
337
358
|
];
|
|
@@ -346,6 +367,20 @@ export const AI_AGENT_FAMILIES = new Set([
|
|
|
346
367
|
'claude-user', 'claudebot', 'gptbot', 'openai', 'perplexity', 'google-extended',
|
|
347
368
|
]);
|
|
348
369
|
|
|
370
|
+
/** Automation / HTTP-tool families that named themselves in the UA. OBSERVATION ONLY —
|
|
371
|
+
* same rule as the rest of this table: never identity, never a verdict. Must match
|
|
372
|
+
* `NAMED_TOOL_FAMILIES` in examples/agent_entry_reference.py. */
|
|
373
|
+
export const NAMED_TOOL_FAMILIES = new Set([
|
|
374
|
+
'camoufox', 'playwright', 'puppeteer', 'selenium', 'headless-chrome', 'curl',
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
/** The four owner-facing traffic classes `clientClass` may return. Bounded on purpose:
|
|
378
|
+
* an attacker-chosen UA must never become a class name. Must match
|
|
379
|
+
* `CLIENT_CLASSES` in examples/agent_entry_reference.py. */
|
|
380
|
+
export const CLIENT_CLASSES = new Set([
|
|
381
|
+
'declared-agent', 'named-tool', 'stealth-agent', 'human-like',
|
|
382
|
+
]);
|
|
383
|
+
|
|
349
384
|
/** ASCII-only lowercase fold. NOT `toLowerCase()`: Unicode casing is runtime- and
|
|
350
385
|
* locale-shaped (the Turkish-I class of surprise), and no needle in the table needs it —
|
|
351
386
|
* folding only A-Z is what makes the same UA string classify identically in both twins. */
|
|
@@ -359,7 +394,7 @@ function asciiLower(s) {
|
|
|
359
394
|
}
|
|
360
395
|
|
|
361
396
|
/** UA string -> family. Absent/empty/non-string -> 'none'; no needle matched -> 'other'.
|
|
362
|
-
* Total on untrusted input, and the RETURN VALUE is always one of the
|
|
397
|
+
* Total on untrusted input, and the RETURN VALUE is always one of the fixed
|
|
363
398
|
* family names — never a substring of the input (bounded stats keyspace). */
|
|
364
399
|
export function uaFamily(ua) {
|
|
365
400
|
if (typeof ua !== 'string' || !ua) return 'none';
|
|
@@ -370,6 +405,23 @@ export function uaFamily(ua) {
|
|
|
370
405
|
return 'other';
|
|
371
406
|
}
|
|
372
407
|
|
|
408
|
+
/** Owner-facing traffic class. OBSERVATION ONLY — never `verified`, never a ledger row,
|
|
409
|
+
* never a rate lane, never a refusal. The split an operator asked for: tell agent
|
|
410
|
+
* traffic from human traffic WITHOUT treating a spoofable UA as a credential.
|
|
411
|
+
*
|
|
412
|
+
* Camoufox / patched Playwright send a clean Firefox UA, so `uaFamily` says `browser`.
|
|
413
|
+
* That is not a human reading the homepage. A human Firefox almost never GETs the
|
|
414
|
+
* agent card or POSTs the door; a stealth agent that found the door does. The one
|
|
415
|
+
* human-shaped case at the door is `browser` + `notice_get` (someone opened the
|
|
416
|
+
* address as a document on a site-owning mount). Must match `client_class` in
|
|
417
|
+
* examples/agent_entry_reference.py. */
|
|
418
|
+
export function clientClass(family, stage) {
|
|
419
|
+
if (AI_AGENT_FAMILIES.has(family) || family === 'muretai-node') return 'declared-agent';
|
|
420
|
+
if (NAMED_TOOL_FAMILIES.has(family)) return 'named-tool';
|
|
421
|
+
if (family === 'browser' && stage === 'notice_get') return 'human-like';
|
|
422
|
+
return 'stealth-agent';
|
|
423
|
+
}
|
|
424
|
+
|
|
373
425
|
/** The FIRST User-Agent value out of a headers mapping, or null. Case-insensitive key
|
|
374
426
|
* scan so an in-process host can pass any casing; Node's own `req.headers` already
|
|
375
427
|
* lowercases keys and keeps only the FIRST user-agent of a duplicated pair — the Python
|
|
@@ -2254,11 +2306,14 @@ export function canonicalMount(canonUrl, basePath) {
|
|
|
2254
2306
|
* callback, and a watcher that dialled out on the hot path would make the
|
|
2255
2307
|
* visitor's answer depend on somebody else's uptime.
|
|
2256
2308
|
*
|
|
2257
|
-
* WHAT IT IS TOLD. Every stage reports `stage`, `identified
|
|
2309
|
+
* WHAT IT IS TOLD. Every stage reports `stage`, `identified`,
|
|
2258
2310
|
* `ua_family` — the door's own bounded classification of the client, one
|
|
2259
2311
|
* of the fixed `UA_FAMILIES` names and NEVER a substring of what the caller
|
|
2260
|
-
* sent, so a stranger cannot write its own label into your metrics
|
|
2261
|
-
*
|
|
2312
|
+
* sent, so a stranger cannot write its own label into your metrics — and
|
|
2313
|
+
* `client_class`, the four-way split (`declared-agent` / `named-tool` /
|
|
2314
|
+
* `stealth-agent` / `human-like`) that tells an operator whether a
|
|
2315
|
+
* Firefox-looking knock is a stealth agent at the door or a human who
|
|
2316
|
+
* opened the notice. The POST stages add the envelope on top.
|
|
2262
2317
|
*
|
|
2263
2318
|
* WHAT NOT TO PUT IN IT. The envelope carries `peer_did`/`owner_did`,
|
|
2264
2319
|
* which a visitor handed you to transact with YOU. Forwarding a raw DID to
|
|
@@ -2498,6 +2553,7 @@ export function createAgentEntry({
|
|
|
2498
2553
|
// composition to any stranger). Keyspace bounded by the fixed UA_FAMILIES table times
|
|
2499
2554
|
// five stage names — an attacker choosing UA strings cannot grow it.
|
|
2500
2555
|
const uaStats = new Map();
|
|
2556
|
+
const clientStatsMap = new Map();
|
|
2501
2557
|
|
|
2502
2558
|
/** Count the stage, and tell the watcher about it.
|
|
2503
2559
|
*
|
|
@@ -2522,9 +2578,14 @@ export function createAgentEntry({
|
|
|
2522
2578
|
let row = uaStats.get(family);
|
|
2523
2579
|
if (!row) { row = new Map(); uaStats.set(family, row); }
|
|
2524
2580
|
row.set(stage, (row.get(stage) || 0) + 1);
|
|
2581
|
+
const cls = clientClass(family, stage);
|
|
2582
|
+
let crow = clientStatsMap.get(cls);
|
|
2583
|
+
if (!crow) { crow = new Map(); clientStatsMap.set(cls, crow); }
|
|
2584
|
+
crow.set(stage, (crow.get(stage) || 0) + 1);
|
|
2525
2585
|
if (typeof observer !== 'function') return;
|
|
2526
2586
|
if (stage === 'card_get' || stage === 'notice_get') {
|
|
2527
2587
|
observe({ stage, identified: 0, verified: false, ua_family: family,
|
|
2588
|
+
client_class: cls,
|
|
2528
2589
|
peer_did: null, owner_did: null, wba_did: null, text: null });
|
|
2529
2590
|
}
|
|
2530
2591
|
}
|
|
@@ -2539,6 +2600,17 @@ export function createAgentEntry({
|
|
|
2539
2600
|
return out;
|
|
2540
2601
|
}
|
|
2541
2602
|
|
|
2603
|
+
/** A plain JSON-able copy of the traffic-class counters: { class: { stage: n } }.
|
|
2604
|
+
* Same contract as `stats()`: in-process only, never on the wire, bounded keyspace. */
|
|
2605
|
+
function clientStats() {
|
|
2606
|
+
const out = {};
|
|
2607
|
+
for (const [cls, row] of clientStatsMap) {
|
|
2608
|
+
out[cls] = {};
|
|
2609
|
+
for (const [stage, n] of row) out[cls][stage] = n;
|
|
2610
|
+
}
|
|
2611
|
+
return out;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2542
2614
|
/** The `Link` header the notice route carries — the SAME one-field value for EVERY
|
|
2543
2615
|
* caller. TWO relations in ONE header field (RFC 8288 allows several link-values in
|
|
2544
2616
|
* one field, and one field is what keeps the two twins' bytes identical through their
|
|
@@ -2843,6 +2915,7 @@ export function createAgentEntry({
|
|
|
2843
2915
|
if (typeof observer !== 'function' || lastRefusal === null) return;
|
|
2844
2916
|
observe({ verified: false, refused: lastRefusal, stage: 'refused_post',
|
|
2845
2917
|
identified: pendingStage === 'signed_post' ? 1 : 0, ua_family: pendingFamily,
|
|
2918
|
+
client_class: clientClass(pendingFamily, 'refused_post'),
|
|
2846
2919
|
peer_did: null, owner_did: null, wba_did: null, text: null });
|
|
2847
2920
|
}
|
|
2848
2921
|
|
|
@@ -3089,6 +3162,7 @@ export function createAgentEntry({
|
|
|
3089
3162
|
// arrived. `identified` is read off the envelope rather than the stage, because the
|
|
3090
3163
|
// anonymous lane answers a visitor who genuinely presented no DID.
|
|
3091
3164
|
observe({ ...env, stage: pendingStage, ua_family: pendingFamily,
|
|
3165
|
+
client_class: clientClass(pendingFamily, pendingStage),
|
|
3092
3166
|
identified: env && env.peer_did ? 1 : 0 });
|
|
3093
3167
|
let answer;
|
|
3094
3168
|
try {
|
|
@@ -3236,6 +3310,24 @@ export function createAgentEntry({
|
|
|
3236
3310
|
// and when this entry is mounted under a path, "anywhere else" INCLUDES the bare
|
|
3237
3311
|
// host, which belongs to the site (or to the neighbour agent) and not to us.
|
|
3238
3312
|
if (!isMountPath(pathname)) return jsonResponse(404, { error: 'not found' });
|
|
3313
|
+
// A QUERY STRING MEANS THE POST IS NOT OURS. The door's address is the signed
|
|
3314
|
+
// card's `url`, byte-exact: a base URL carrying a query is refused at startup, and
|
|
3315
|
+
// a visitor's walk drops any query it was handed before it POSTs the card's `url` —
|
|
3316
|
+
// so no conformant caller can arrive here, while a site's own query-multiplexed
|
|
3317
|
+
// traffic (`?wc-api=`, `?wc-ajax=`, `?rest_route=`) always does. Disjoint BY
|
|
3318
|
+
// CONSTRUCTION, which is what makes this a rule and not a heuristic. Measured:
|
|
3319
|
+
// WooCommerce Stripe delivers its only webhook to `/?wc-api=wc_stripe` (path `/`,
|
|
3320
|
+
// application/json); a bare-origin door that claimed it answered HTTP 200/-32601
|
|
3321
|
+
// echoing the event id, the sender recorded the event as delivered and never
|
|
3322
|
+
// retried — payment events lost SILENTLY. The answer is the unowned-path 404
|
|
3323
|
+
// above, same bytes, one meaning — the DECISION(non-door-post-answers-404-not-405)
|
|
3324
|
+
// non-disclosure again, because the caller most likely to land here is the site's
|
|
3325
|
+
// own webhook, which deserves the most anonymous answer, never a door verdict.
|
|
3326
|
+
const hashless = target.split('#')[0];
|
|
3327
|
+
const queryAt = hashless.indexOf('?');
|
|
3328
|
+
if (queryAt !== -1 && queryAt + 1 < hashless.length) {
|
|
3329
|
+
return jsonResponse(404, { error: 'not found' });
|
|
3330
|
+
}
|
|
3239
3331
|
const buf = bodyBuffer || Buffer.alloc(0);
|
|
3240
3332
|
const out = handlePost(buf, headers);
|
|
3241
3333
|
// The stage is read off the finished answer, so an async responder tallies when it
|
|
@@ -3367,9 +3459,10 @@ export function createAgentEntry({
|
|
|
3367
3459
|
|
|
3368
3460
|
// `mount` is exported so a host app can route exactly what this entry answers (and log
|
|
3369
3461
|
// it): it is derived, so reading it here can never disagree with the signed card.
|
|
3370
|
-
// `stats` is the owner-facing UA-family counters
|
|
3371
|
-
//
|
|
3372
|
-
|
|
3462
|
+
// `stats` is the owner-facing UA-family counters, `clientStats` the four-way
|
|
3463
|
+
// traffic-class split, and `wbaVisits` the DID->count of WBA-verified fetches —
|
|
3464
|
+
// all in-process only, like `ledger`.
|
|
3465
|
+
return { did, card, ledger, mount, stats, clientStats, wbaVisits,
|
|
3373
3466
|
handleRequest, handleRequestAsync, listen,
|
|
3374
3467
|
cardEnvelope: () => JSON.parse(cardEnvelopeBytes().toString('utf8')) };
|
|
3375
3468
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muretai/agent-entry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Make your website answer AI agents: an A2A agent endpoint that verifies who is knocking, opens an account for them and replies signed, in one HTTP round trip. Zero dependencies. Pairs with llms.txt and WebMCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "muretai-agent-entry.mjs",
|